[PR]小規模ECサイトに最適なWAF、SiteGuard Lite
2009年10月19日 [Perl][PHP][SQL]
_quoteメソッドの数値データ対応を検証する
このエントリでは、PerlのDBI、PHPのPDO、MDB2にて用意されているquoteメソッドが数値データをどのように扱えるかを検証しました。結論としてMDB2が合格、それ以外は不合格で、とくにDBD::mysqlを使用した場合、脆弱性といってもよいような結果となりました。
概要
DBI、PDO、MDB2は、いずれもデータベースアクセスを抽象化したモジュール(クラス)であり、汎用的な記述によりさまざまなデータベースを利用できるように工夫されています。これらモジュール(クラス)にはquoteというメソッドが用意されています。DBIのquoteメソッドの呼び出し例を示します。
my $dbh = DBI->connect('DBI:mysql:dbname:localhost', 'user', 'pass');
print $dbh->quote("a\\'"); # 「a\'」という文字列を指定
【処理結果】
'a\\\''
ごらんのように、あるいはquoteという名称が示すように、これらメソッドは入力データをSQL文字列リテラルとしてエスケープした上で、シングルクオートでくくります。SQLの文字列リテラルのエスケープ方法はデータベースソフトウェア依存であり、MySQLの場合はシングルクオートとバックスラッシュをエスケープしますが、標準SQLではシングルクオートのみです*1。quoteメソッドを利用することにより、データベースの種類を自動的に考慮して、SQLを安全かつ簡便に動的組み立てできます。
さて、quoteメソッドには省略可能な第二引数があり、データの型を指定できるようになっています(デフォルトは文字列型)。以下に、整数型を指定した場合の呼び出し方を示します。
DBI: $dbh->quote($n, SQL_INTEGER) PDO: $dbh->quote($n, PDO::PARAM_INT) MDB2: $dbh->quote($n, 'integer')
ごらんのように、呼び出し方はほとんど同じです。では、この処理結果は、どうなる *べき* でしょうか。私の過去の日記「数値項目に対するSQLインジェクション対策のまとめ」や「SQLの暗黙の型変換はワナがいっぱい」で説明したように、SQLは本来厳格な型をもった言語であり、数値リテラルをシングルクオートで囲むことは好ましくありません。したがって、quoteメソッドに数値型を指定した場合は、たんなる数値が返るべきだと考えます(SQL組み立てが目的なので、型は文字列型でもよい)。そこで、入力データのサンプルとして「1a\'」を第一引数に、整数型指定を第二引数としてquoteメソッドを呼び出してみました。
DBI: $dbh->quote("1a\\'", SQL_INTEGER)
PDO: $dbh->quote("1a\\'", PDO::PARAM_INT)
MDB2: $dbh->quote("1a\\'", 'integer')
結果は以下のようになります。PDOとMDB2は、MySQLとPostgreSQLで同じ結果です。
| モジュール名 | 結果 |
|---|---|
| DBI(DBD::mysql) | 1a\' |
| DBI(DBD::PgPP) | '1a\\\'' |
| PDO | '1a\\\'' |
| MDB2 | 1 (int型) |
先に書いた「あるべき結果」になったのはMDB2だけでした。PDOとDBD::PgPPは、文字列型として指定したのと同じ結果であり、SQLインジェクション対策として機能しますが、なんのためにわざわざ整数型を指定したのかわかりません。DBD::mysqlはエスケープもクオートもしていないので、入力として「1 union select table_name from information_schema.tables」を与えられると、一覧表を表示する画面でテーブルの一覧が表示させられるような結果となります。これはまずい。現実問題として、何もしないのでは、わざわざquoteをメソッドを呼ぶ動機がありません。
また、文字列としてエスケープする方法も、先のエントリで説明したように、さまざまな副作用がありますので、quoteメソッドを使うよりは、SQL呼び出し直前でバリデーションするか、sprintfで%dの書式を与えることで、数値のみが出力されるように制限する手法が考えられます。
今回の調査で、MDB2のquoteメソッドは、わりといい線行っていると思いました。MDB2のquoteでは、'decimal'という指定もできるのですが、20桁程度の数値を与えても、破綻なく動作します。
| 入力 | 出力 |
|---|---|
| 123456789012345678901 (文字列型) | 123456789012345678901 (文字列型、クオートなし) |
| 1a2b3 (文字列型) | 123 (文字列型、クオートなし) |
'1a2b3'に対する結果が「123」となるところ、「サ、サニタイズかよ」と思ってしまいますが、まぁ数値としてのバリデーションは入り口でやるとして、SQL発行時の「最後の砦」としては、まぁ許容できる範囲ではないかと思います。「例外を発生させるべきじゃないの?」と思うのですが、他のモジュールのだめさ加減との比較では上出来でしょう。
なお、DECIMAL型の場合に、文字列型で結果を返すのは妥当な処理です。PHPに標準で用意されているintegerやfloatでは、DECIMALの桁数(MySQLでは最大65桁、PostgreSQLでは最大1,000桁)を表現するのに十分なビット長がないからです。演算はどうするのだという疑問がありそうですが、DBの性能に余裕があればSQLで演算するという手もありますし、BCMath 任意精度数学関数を使う手もあります。BCMathは文字列型のままで数値演算ができます。
まとめ
DBI、PDO、MDB2のquoteメソッドについて、第二引数明示により数値として処理する内容について検証しました。期待どおり動作するのはMDB2であり、一方DBD::mysqlではなにもしていないので脆弱性対策としては使えないことがわかりました。
また、今回のエントリでは示していませんが、PDOは処理が高速なために人気があるものの、内部処理をつっこんで見ていくと、不安が出てきました。その内容については別途何かの形で発表したいと思いますが、現時点では、id:moriyoshiさんの以下のエントリが参考になると思います。
*1 詳細については、「 SQLのエスケープ再考 」および「SQLエスケープにおける「\」の取り扱い 」に詳しく書きましたので参考になさってください
[PR]小規模ECサイトに最適なWAF、SiteGuard Lite