(Last Updated On: 2022/05/17)
大抵のシェルスクリプトはセキュリティ対策を考慮する必要がないので与えられたパラメーターはそのまま利用されています。シェルスクリプトを利用して権限の無いユーザーが勝手なコマンドを実行できないようにするにはエスケープが必要になります。
- setuid/setgidをしたシェルスクリプト
- 信頼できない入力を処理するシェルスクリプト
この様な場合はエスケープ(やバリデーション)が必要になります。
シェルスクリプト特殊文字
文字列を正しく安全に処理するには特別な意味を持つ特殊文字を理解する必要があります。どのような言語でも同じです。特殊文字を理解し、特殊文字を適切にエスケープすれば、文字列リテラルをコマンドとして意図しない処理をしてしまうバグを防止できます。
UNIX Power Toolsによるとシェルの特殊文字には以下のようなモノあります。
Character Where Meaning ESC csh Filename completion. RETURN csh, sh Execute command. space csh, sh Argument separator. TAB csh, sh Argument separator. TAB bash Filename completion. # csh, sh Start a comment. ` csh, sh Command substitution (backquotes). " sh Weak quotes. " csh Weak quotes. ' sh Strong quotes. ' csh Strong quotes. \ sh Single-character quote. \ csh Single-character quote. $var csh, sh Variable. ${var} csh, sh Same as $var. $var:mod csh Edit var with modifier mod ${var-default} sh If var not set, use default. ${var=default} sh If var not set, set it to default and use that value. ${var+instead} sh If var set, use instead. Otherwise, null string. ${var?message} sh If var not set, print message (else default). If var set, use its value. ${var#pat} ksh, bash Value of var with smallest pat deleted from start. ${var##pat} ksh, bash Value of var with largest pat deleted from start. ${var%pat} ksh, bash Value of var with smallest pat deleted from end. ${var%%pat} ksh, bash Value of var with largest pat deleted from end. | csh, sh Pipe standard output. |& csh Pipe standard output and standard error. ^ sh only Pipe character (obsolete). ^ csh, bash Edit previous command line. & csh, sh Run program in background. ? csh, sh Match one character. * csh, sh Match zero or more characters. ; csh, sh Command separator. ;; sh End of case statement. ~ csh, ksh, bash Home directory. ~user csh, ksh, bash Home directory of user. ! csh, bash Command history. - Programs Start of optional argument. - Programs Read standard input. (Only certain programs.) $# csh, sh Number of arguments to script. "$@" sh Original arguments to script. $* csh, sh Arguments to script. $- sh Flags set in shell. $? sh Status of previous command. $$ csh, sh Process identification number. $! sh Process identification number of last background job. $< csh Read input from terminal. cmd1 && cmd2 csh, sh Execute cmd2 if cmd1 succeeds. cmd1 || cmd2 csh, sh Execute cmd2 if cmd1 fails. $(..) ksh, bash Command substitution. ((..)) ksh, bash Arithmetic evaluation. \. file sh Execute commands from file in this shell. : sh Evaluate arguments, return true. : sh Separate values in paths. : csh Variable modifier. [] csh, sh Match range of characters. [] sh Test. %job csh, ksh, bash Identify job number. (cmd;cmd) csh, sh Run cmd;cmd in a subshell. {} csh, bash In-line expansions. {cmd;cmd; } sh Like (cmd;cmd) without a subshell. >file csh, sh Redirect standard output. >>file csh, sh Append standard output. <file csh, sh Redirect standard input. <<word csh, sh Read until word, do command and variable substitution. <<\word csh, sh Read until word, no substitution. <<-word sh Read until word, ignoring leading TABs. >>! file csh Append to file, even if noclobber set and file doesn't exist. >! file csh Output to file, even if noclobber set and file exists. >| file ksh, bash Output to file, even if noclobber set and file exists. >& file csh Redirect standard output and standard error to file. m> file sh Redirect output file descriptor m to file. m>> file sh Append output file descriptor m to file. m< file sh Redirect input file descriptor m from file. <&m sh Take standard input from file descriptor m. <&- sh Close standard input. >&m sh Use file descriptor m as standard output. >&- sh Close standard output. m<&n sh Connect input file descriptor n to file descriptor m. m<&- sh Close input file descriptor m. n>&m sh Connect output file descriptor n to file descriptor m. m>&- sh Close output file descriptor m.
通常、文字列リテラルはシングルクオートかダブルクオートで囲みます。これらの文字全てを文字列リテラルの中でエスケープする必要はありませんが、予期しない変数展開を防ぐにはシングルクオートとダブルクオートをエスケープするだけではダメであると分かります。
クオート文字を使わずに文字列リテラルを安全に利用するのは著しく困難であることは、上の特殊文字一覧で良く分かると思います。もしもクオートせずに文字列リテラルを使いたい場合、バリデーションして絶対に安全であることを保障してから使います。
シェルスクリプト文字列のエスケープ
先の特殊文字一覧からシェルが変わると特殊文字も変わると分かります。シェル仕様の違いを全て理解して安全にエスケープするのは手間がかかります。安全なシェルスクリプトを作りたいプログラマーとしては「文字列リテラルが文字列リテラルであること」を保障できるエスケープ方法を理解しているだけでOKです。
printfコマンドの%qが利用できます。
%q Output the corresponding argument in a format that can be
reused as shell input
%qによるエスケープは安全に出力できるモノだけそのまま出力(ホワイトリスト方式で出力)し、他をエスケープしてくれます。
printf命令でクオート済みに整形(エスケープ)して出力する方法が良いでしょう。以下の2つはシェルスクリプト引数のhost名とipアドレスをエスケープしています。
printf -v host "%q" $1
printf -v ip "%q" $2
host、ipは文字列リテラルの中に埋め込んでも誤ってコマンドとして解釈されることがなくなり、マニュアルに記載の通りシェルの入力として安全に利用できる形にエスケープされています。
文字列以外のフォーマット
printfは文字列以外にもで整数や浮動小数に整形することも可能です。数値をクオートして利用するのが不適切な場合に便利です。
$ printf -v v "%d" "123abc"; echo $v -bash: printf: 123abc: 無効な数字です 123 $ printf -v v "%d" "x01234"; echo $v -bash: printf: x01234: 無効な数字です 0 $ printf -v v "%d" "0x1234"; echo $v 4660
$ printf -v v "%f" "123.456abc"; echo $v -bash: printf: 123.456abc: 無効な数字です 123.456000 $ printf -v v "%f" "123.456"; echo $v 123.456000 $ printf -v v "%f" "123e10"; echo $v 1230000000000.000000
利用可能なフォーマットには以下のようなモノがあります。
%b
– Print the argument while expanding backslash escape sequences.%q
– Print the argument shell-quoted, reusable as input.%d
,%i
– Print the argument as a signed decimal integer.%u
– Print the argument as an unsigned decimal integer.%o
– Print the argument as an unsigned octal integer.%x
,%X
– Print the argument as an unsigned hexadecimal integer.%x
prints lower-case letters and%X
prints upper-case.%e
,%E
– Print the argument as a floating-point number in exponential notation.%e
prints lower-case letters and%E
prints upper-case.%a
,%A
– Print the argument as a floating-point number in hexadecimal fractional notation.%a
prints lower-case letters and%A
prints upper-case.%g
,%G
– Print the argument as a floating-point number in normal or exponential notation, whichever is more appropriate for the given value and precision.%g
prints lower-case letters and%G
prints upper-case.%c
– Print the argument as a single character.%f
– Print the argument as a floating-point number.%s
– Print the argument as a string.%%
– Print a literal%
symbol.
注意事項
printfはコマンドなのでシステムによって仕様が異なります。Linuxの場合は一般的にはcoreutilsのprintfが使わているのでここで紹介した通りの動作のはずです。使用するprintfコマンドが期待する動作をするか確認してから利用します。
printfの%qを利用するとクオート無しでも一つの文字列リテラルと扱えるようにエスケープしますが、文字列は基本的にはクオートして利用する方が良いでしょう。
$ printf -v v "%q" "abc def"; echo $v abc\ def $ printf -v v "%q" "abc def"; echo "$v" abc\ def
文字列をクオートして利用する方が良いのは、もし空文字列だった場合に引数の順序が変わってしまい、それ以前のバリデーション処理を台無しにしたりするからです。
今は全てUTF-8だと思うので文字エンコーディングが問題になることは稀だと思いますが、SJISを使わなければならない、といった場合にはエンコーディングのバリデーションするべきでしょう。