▼2010年7月1日木曜日ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)補足この記事は旧徳丸浩の日記からの転載です。元URL、アーカイブ、はてなブックマーク1、はてなブックマーク2。備忘のため転載いたしますが、この記事は2010年7月1日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。補足終わりPHPのデータベース・アクセス・ライブラリPDOは、DB接続時の文字エンコーディング指定ができないため、文字エンコーディングの選択によっては、プレースホルダを使っていてもSQLインジェクション脆弱性が発生します。ここに来て急にブクマが追加されはじめていますが、このエントリを書いてから状況が改善しています。PHP5.3.6(2011/03/17)にて、PDOでもデータベース接続の文字エンコーディングを指定できるようになりました。この版で、UNIX版のPHPでは解決しましたが、Windows版のPHPではバグがあり、解決には至っていませんでした。その後、千葉征弘さん(@nihen)により原因が判明し、PHP5.3.7RC1では修正されていることを確認しています。したがって、PHP5.3.7の正式版が出れば、この問題は解決すると思われます。(追記終わり)はじめに本を書いています。SQLインジェクションの節から書き始めて、初稿はレビュアーの方々にお送りしましたところ、さっそく有意義なコメントを多数頂戴しています。ありがとうございます。その原稿の中で、DBアクセスに使用するPHPのライブラリとしてMDB2を紹介していたところ、PDOを紹介すべきではという意見を複数頂戴しました。そこで、現時点でPDOを採用していない理由を報告したいと思います。これは、「安全なSQLの呼び出し方」でPDOを取り上げていない理由でもあります。PDOは接続時に文字エンコーディングを指定できない(指定しても無視される)ので、データベースへの接続時の文字エンコーディングはLatin1が暗黙に指定されます。SJIS」により、文字エンコーディングを接続後に指定するようなプログラミングがなされているようです(MySQLの場合)。そのようなプログラム例を示します。このプログラムでは、内部文字エンコーディングとしてShift_JISを利用しているという想定です。PHP5.3.0とMySQL5.1.37の組み合わせで確認しました。}PDO+MySQLの場合、プレースホルダは「動的プレースホルダ」あるいは、俗に「クライアントサイドのプリペアードステートメント」などと呼ばれる実装になっています*1。1=1#」という文字列を指定した場合、MySQLに対して以下のようなクエリが送信されます(Wiresharkによるダンプ)。0x83は「ソ」の先頭バイトです。これをPDOではLatin1(ISO-8859-1)として扱うので、0x83はNBHという制御文字を意味し、1バイト文字になります。PDOは、続く「\'」を「\\\'」とエスケープします。MySQLサーバー側では、受け取ったSQLをShift_JISと想定しているので「ソ\\'」という並びと解釈します。1=1#'」はSQL文の一部とみなされます。SQL文が注入できたので、SQLインジェクション脆弱性があるということになります。ここまでの説明を下図にまとめました。names」がセキュリティ上問題があることが広く知られるようになったせいか、PDOの文字化けを別の方法で対処するようにすすめているエントリもあります。このエントリでは、my.cnf(my.ini)の修正でPDOの文字化けに対応しているのですが、Shift_JISを使う場合はこれもダメです*2。その理由は、「PDOはLatin1として処理したものを、MySQLはShift_JISとして解釈する」状態には変わらないからです。まとめPDO+MySQLでは、動的プレースホルダ(なんちゃってプリペアード・ステートメント)が使われるPDOでは文字エンコーディングが指定できないPDO+MySQL+Shift_JISでは、プレースホルダを使ってもSQLインジェクション脆弱性となる解決策・回避策PDOの文字化け問題とSQLインジェクション脆弱性問題を解決するにはどうすればよいでしょうか。私が思いつくのは以下の2つの方法です。utf8により文字化けを回避する(回避策)2.で具体的な問題が起こるかどうかは分かりませんが、文字エンコーディングを正しく扱えないことで潜在的なリスクがあると考えます。そのため、書いている本の中では1.を採用したという訳です。私自身PHPを熟知しているわけではないので、ひょっとすると、PDOの使い方などでもっとよい方法があるかもしれません。識者のご指摘をいただければ幸いです。22:20)id:nihenさんが、PDOに対して文字エンコーディングを設定する方法を調べてくださいました。nihenさん、どうもありがとうございます。PDOしたあとにあとから$dbh->setAttributeしても、ダメ。APIに渡すパラメータとして、my.cnfのファイル名を指定しています。上記の例ではGROUPを「client」に設定していますので、my.cnfに以下の設定を追加することになります。[client]default-character-set=sjisただし、Windows版のPHPでは、PDO::MYSQL_ATTR_READ_DEFAULT_FILEが使えないようです。このため、サーバー接続時に文字エンコーディングを指定する方法がありません。このため、Windows版のPHPを使う場合は、PDO::ATTR_EMULATE_PREPARESをfalseにする(真のプリペアード・ステートメントを使う)ことで対策することになります。12:23)再現しなかったので、もう少し現象が再現できる設定を教えていただけると幸いです。私は下記のような環境で試しました。1=1#"}■バージョンMySQL:5.1.34PHP:5.3.2ちなみにPHPのソースコード、内部エンコーディングなどはShift_JISで統一しています。今回のSQLインジェクションが成功するのであればvar_dumpの結果は「1」になる予定でSQLを変更しています。13:26)kanehamaさん、こんにちは再現テストをしていただき、ありがとうございます。当方では、SQLインジェクションとなるようです。latin1この状態ですと、データベースの日本語に対して正常にアクセスできないのではないでしょうか。当方の条件を変えて、上記エンコーディングをlatin1になるように調整すると、SQLインジェクションは出ない代わりに、日本語が文字化けします。お手数ですが、いったんテーブルに格納した日本語が正常に読み書きできるかご確認いただけないでしょうか。15:09)徳丸浩さんありがとうございます。確認してみると確かに書き込み/読み込みで文字化けていました。(確認が足りなくて申し訳ありません。MySQLの文字コード関連の設定は下記のようになっています。utf8ただし、同じPHPソースを別環境で実行すると、SQLインジェクションは起きませんでした。MySQLの文字コード設定は下記のようになっています。5.3から何か変更あったのですかね。5.3.2が入ってる環境のPHPのバージョンを5.2.3に変えて確認してみます。ネイティブにサポートしているという利点があります。この機能をエミュレートします。19:41)PHP5.2.1から、PDO_MYSQLは、デフォルトではクライアントサイドのプリペアードステートメントになったようですね。プリペアードステートメントだと、MySQLのクエリキャッシュが効かないためのようです。こちらに動作検証された方がいらっしゃいます。