PHPでの入力値チェックのすり抜け

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は、開発者が変数の型を意識する必要のある言語です(皮肉なことに、型に厳密な言語よりも)。

世間様でよく「PHPは型のない言語」だとかなんとかいう風評が流されておりますが。とんでもハップンでございます。

PHPは、素晴らしく「型の厳密な」言語でございますっていうか「型を厳密にプログラマが意識しておかないといかん言語」でございます。

そうでなければ何故に gettype などという関数が用意されているとおっしゃるのでしょうか? is_int()が、is_string()が、用意されているとお思いでしょうか?
がるの健忘録(2006/11/08) - 素晴らしき自動的な世界〜或いは「型のない」世界〜

  • エスケープ処理と形式チェックは別物
    上記では、SQLインジェクションを匂わす例をいくつか挙げました。文脈上エスケープ処理が必要な場面で、形式チェックに頼ってエスケープ処理を省略するのは、良い習慣ではないと思います(SQLに限らず)。上記例のようなものを含めて、形式チェックには思ったよりも実装ミスが多いからです。
    またそれ以上に、通常の形式チェック処理は、エスケープ処理とは異なる観点・目的で行なうものだからです。SQLインジェクション除けにはエスケープ*1、業務仕様外のデータの登録防止などには形式チェックを行ないます。

*1:可能ならPrepared Statementを使う。