PHPでのSQLインジェクション対策 - エスケープ・クォート編: MDB2
PEAR::MDB2
MDB2はPHPでデータベースを操作するためのPEARのパッケージの1つです。MDB2の簡単な説明や、コネクトの方法、フェッチモードの設定などはプレースホルダ編を参照していただくことにして、ここでは実際にクォートをする例を説明します。
MDB2でクォートする
まずはSQLに埋め込む値をそれぞれ型にあわせてクォートする例を見てみましょう。
$res = $mdb2->query( 'UPDATE users’ . ' SET profile = ' . $mdb2->quote($_REQUEST['profile'], 'text') . ' WHERE userid = ' . $mdb2->quote($_SESSION['me']['userid'], 'integer') ); $res->free();
$all = $mdb2->queryAll( 'SELECT userid, username, profile' . ' FROM users WHERE username LIKE ' . $mdb2->quote('%' . $mdb2->escapePattern($_REQUEST['username']) . '%', 'text'), array('integer', 'text', 'text') );
quoteメソッド
MDB2のquoteメソッドでは、1つめの引数にデータそのものを、2つめの引数にデータ型を指定します。第2引数のデータ型は省略可能ですが、省略した場合は第1引数の型がPHP的に判定され、その結果、integer、boolean、textなどと言った型でクォートされます。
ただ気をつけなければならないのは、$_REQUEST、$_GET、$_POST、$_COOKIEなど、ブラウザから渡ってくる値は(配列型のキーを除けば)必ず文字列だということです。これらの変数を第1引数に設定し、第2引数を省略した場合、必ず文字列としてクォートされます(例: '256')。比較対象のカラムがinteger型の場合には、暗黙の型変換が発生しますので、インデックスが使用されない、数値の比較時に意向した通りに動いてくれないなどの問題が発生し得ます。SQLを発行する前に、ちゃんと変数の内容が妥当かチェックすることも有効ですし、それと同時に、できるだけquoteの第2引数に型を設定するようにしましょう。
文字列リテラル、数値リテラルなど、SQLに埋め込むフォーマットとして適切なものにquoteメソッドを使って変換していく最、第2引数に指定できるデータ型の詳細は、「PEAR::データ型の処理の概要」を参照してください。
更新系にはqueryメソッド
INSERT、UPDATE、DELETEなど、データ操作系のSQLにはqueryメソッドを使います。この後もスクリプトが続くのであれば、freeメソッドでメモリを開放してやるのがよいでしょう。
検索系にはquery***メソッド
SELECTの結果を取得するのに今回はqueryAllメソッドを使いました。主キー指定した検索のように、結果が必ず1行以下になるようなSQLであればqueryRowでも良いでしょうし、他にも、queryOne、queryColといったメソッドもあります。またこれらのquery***メソッドの2つめの引数には、検索結果の戻り値にデータの型を指定することができます。上記の例の array('integer', 'text', 'text') はそれぞれ、userid, username, profileが対応しています。この型も省略可能ですが、省略した場合、戻り値は全てtextとして扱われます。また、query***の内部では、結果に対して自動的にfreeメソッドを発行しています。
検索をqueryメソッド+fetch***メソッドで
上記のqueryAllを使ったSELECT文の流れを、queryメソッドで書きなおしてみると、だいたい以下のような流れになります。
$res = $mdb2->query( 'SELECT * FROM users' . ' WHERE username' . ' LIKE ' . $mdb2->quote('%' . $mdb2->escapePattern($_REQUEST['username']) . '%', 'text'), array('integer', 'text', 'text') ); $all = $res->fetchAll(); $res->free();
もし、検索結果のデータを全て$allに格納してしまうことが問題であれば $all = ... の部分を
while(($row = $res->fetchRow())) { // それぞれの行に対する処理 }
と書くことも可能です。
LIKEにはescapePatternメソッドを
またここではLIKE演算子の例をとりあげました。プレースホルダ編の繰り返しになりますが、LIKE演算子は「%」を0文字以上の文字列とマッチする、「_」はすべての一文字とマッチするパターン文字として判断します。例えば「%」という文字列とマッチさせたいという意味で入力した「%」であっても、これを単純に「%」ではさみ「%%%」でLIKE検索してしまうと、NULL以外の全ての文字列にマッチしてしまいます。これらのパターン文字を無効化、つまりエスケープしてくれるのがescapePatternメソッドです。バックスラッシュがエスケープ記号として設定されていた場合、「%」は「\%」に、「_」は「\_」に、「\」は「\\」にエスケープされます。エスケープされた文字はLIKEでの検索文字列内でも、パターン文字としてではなく、元々の文字として判断されるという仕組みです※2。
混同しやすいのですが、ここで述べているLIKEのパターン文字のエスケープと、standard_conforming_stringsの設定で変化するシングルクォート・その他の特殊文字のエスケープとは、全く別の階層のものです。例のように、まずはLIKEに指定するパターン文字列を全体を、パターン文字を意識しつつescapePatternメソッドを使って作り、その後で全体へのクォートをquoteメソッドでおこなってください。
※2 Shift_JISなどの、エスケープ記号(この場合はバックスラッシュ)が通常の文字の2バイト目として含まれている文字エンコーディングでは、なかなか正しくエスケープされないのが実情です。実際(執筆時点、2011-04-03)escapePatternは、SJISを使った場合は正しく動作しません。その他の場合にもShift_JISはエスケープ時に問題が発生することが多いので、UTF-8、EUC-JPなどをPHPの内部エンコーディングとして使うようにしてください。
standard_conforming_stringsに関する注意
なおMDB2のquoteメソッドでは、PostgreSQLでの文字列型のエスケープにおいて、PHPネイティブ関数のpg_escape_stringを使用しています。先程も説明しましたが、この関数ではpostgresql.confのstandard_conforming_stringsの設定値に連動して、バックスラッシュのエスケープをするかしないかを自動的に決定してくれるので、安心です。プログラム内で、\n、\tなどを使用している場合には、手動でSQL非標準のE'...'の方式にエスケープ・クォートすることもできるでしょうが、データベースのバージョンやその他の環境によってエスケープ方法が変わることもありますので、おすすめしません。それぞれのバイナリを渡すようなコードに書き換えるか、standard_conforming_stringsにoffを設定しましょう。
MDB2でクォートする時のまとめ
- PHPの内部エンコーディングと、PostgreSQLの文字セットはできるだけUTF-8、少なくともEUC-JPを使いましょう。
- 変数、とくにブラウザやその他のユーザから入力される値は、事前に妥当性のチェックをし、SQLに渡すときには必ずquoteメソッドでクォートしましょう。この時、できるだけ第2引数のデータ型を指定するようにしましょう。