Webアプリケーションでは、外部からの変数に対して、形式チェック(Validation)を行ないます。PHPでこれを行なう場合に、ありがちなミスをいくつか挙げてみました。
この日記は、がるさんの日記に触発されて書いたもので、いくつかの例を引用しています。
がるの健忘録(2006/11/08) - 素晴らしき自動的な世界〜或いは「型のない」世界〜
型の問題
数値と文字列の比較
<?php $input = "2'; DELETE FROM hoge; --"; if ($input == 2) { // ↑TRUEと評価される
がるさんの日記で紹介されていた例に、手を加えたものです。
if文中の式がTRUEになるのは、PHPの「==」演算子が、数値型と文字列型変数を比較する際に、文字列を(かなり強引なやり方で)数値型に変換するからです。変数の比較は、同じ型同士で行なうのが無難だと思います。
<?php $whitelist = array(1, 2); $input = "2'; DELETE FROM hoge; --"; if (in_array($input, $whitelist)) { // ↑TRUEと評価される
がるさんの日記のコメント(byかずくんさん)を、ほぼそのまま引用したものです。これは、先ほどの例と同じ原理で問題が生じています。
in_array関数の三番目の引数にTRUEを与えると、型も考慮した一致判定を行ないます(そうすると、上記のプログラムは正常系も動かなくなりますが)。
なお、array_search関数も同様の動作をします。
文字列同士の比較
<?php // DBから取得したパスワード $db_pass = "01203"; if ($input_pass == $db_pass) { // ↑$input_passが"01203"以外でも // TRUEになる場合がある
これは、がるさんの日記に私がコメントしたものを、アレンジしたものです。
$input_passが、"1203"、"0x4b3"、"+12030.0e-1"などの値の場合に、if文中の式がTRUEになります。これは、PHPの「==」演算子が文字列同士を比較する際に、左右とも数値とみなせる場合は、数値にして比較するからです。
パスワードなどの厳密な取扱いを要する文字列では、strcmp関数や「===」演算子を使いましょう。
正規表現
ereg系関数
<?php // \0はNUL文字 $input = "2\0'; DELETE FROM hoge; --"; if (ereg("^[0-9]$", $input)) { // ↑TRUEと評価される
ereg系関数がバイナリセーフではないことを利用した攻撃で、割と広く知られている方法です。
バイナリセーフでない関数は、他にもいくつかあります(PHP と Web アプリケーションのセキュリティについてのメモを参照)。
これには、NULバイトを事前に排除する(含まれていたらエラーにする)対策が良いと思います。その上で、バイナリセーフで、高速・多機能なPCRE関数(preg_matchなど)を使いましょう。
なお、mb_ereg系関数はバイナリセーフです。
preg系関数
<?php $input = "tera\n"; // \nはLF文字 if (preg_match("/^[a-z]+$/", $input)) { // ↑TRUEと評価される
非常にありがちなコードですが、問題が潜んでいます。
「$」は、末尾のLF文字の直前にもマッチするため、末尾にLFが付いている文字列を通してしまうのです(Perlも同じのはず)。
SQLインジェクションやXSSの心配はありませんが(多分)、LF文字が特殊文字としての機能を持つ場面はあります。例えば、LFがレコードセパレータのデータファイルや、system関数などで実行するshellコマンドなどに、うかつにLF文字を含む文字列を挿入すると、エラーが発生するかもしれません。
あるいは、会員制サイトで、"tera"に対して"tera\n"という会員IDを作成し、見た目上(あるいはシステム上)で両者を混同させるような攻撃もありえます。
PHPでは、パターン修飾子「D」を使うことで、この問題を回避できます(事前に変数をtrimしておくことでも避けられます)。
まとめ
世間様でよく「PHPは型のない言語」だとかなんとかいう風評が流されておりますが。とんでもハップンでございます。
PHPは、素晴らしく「型の厳密な」言語でございますっていうか「型を厳密にプログラマが意識しておかないといかん言語」でございます。
そうでなければ何故に gettype などという関数が用意されているとおっしゃるのでしょうか? is_int()が、is_string()が、用意されているとお思いでしょうか?
がるの健忘録(2006/11/08) - 素晴らしき自動的な世界〜或いは「型のない」世界〜
- エスケープ処理と形式チェックは別物
上記では、SQLインジェクションを匂わす例をいくつか挙げました。文脈上エスケープ処理が必要な場面で、形式チェックに頼ってエスケープ処理を省略するのは、良い習慣ではないと思います(SQLに限らず)。上記例のようなものを含めて、形式チェックには思ったよりも実装ミスが多いからです。
またそれ以上に、通常の形式チェック処理は、エスケープ処理とは異なる観点・目的で行なうものだからです。SQLインジェクション除けにはエスケープ*1、業務仕様外のデータの登録防止などには形式チェックを行ないます。
*1:可能ならPrepared Statementを使う。