補足この記事は旧徳丸浩の日記からの転載です(元URL、アーカイブ、はてなブックマーク1、はてなブックマーク2)。備忘のため転載いたしますが、この記事は2010年9月27日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。補足終わりPHPカンファレンス2010にて「文字コードに起因する脆弱性とその対策」というタイトルで喋らせていただきました。プレゼンテーション資料をPDF形式とslideshare.netで公開しています。文字コードのセキュリティというと、ややこしいイメージが強くて、スピーカーの前夜祭でも「聴衆の半分は置いてきぼりになるかもね」みたいな話をしていたのですが、意外にも「分かりやすかった」等の好意的な反応をtwitter等でいただき、驚くと共に喜んでいます。土曜にPHPカンファレンスに来られるような方は意識が高いというのもあるのでしょうね。その場で少し触れた「文字コードをやや安全に扱う方法」を紹介したいと思います。実はこの方法を紹介するかどうかは悩みどころでして、完全に安全になるならともかく、「これで全部おk」みたいに開発者が文字コードのセキュリティに無関心になる方法に働くといやだなと思うわけです。ネットには、既に根拠の不明確なバッドノウハウであふれているわけで、少しマシなバッドノウハウを流通させることで、改善になるのではないかと思い、公開に踏み切りました。専門家として、どこまで安全にできるかとか、安全になる根拠は示したいと思います。文字コードをやや安全に扱うphp.ini設定まずは結論を示します。php.iniに(あるいは.htaccessで)以下を設定します。"?"php.iniで指定できない条件として以下も必須です。htmlspecialcharsの第3引数に必ず'UTF-8'を指定するPHPのソースはUTF-8でセーブするデータベースの接続・テーブル・データベースの文字エンコーディングは全てUTF-8で統一するこの設定の要点を箇条書きで説明します。Webアプリケーションの入口から出口まですべてUTF-8で統一する入力時にUTF-8→UTF-8の変換をすることで、不正な文字エンコーディングを除去するHTTPレスポンスの文字エンコーディングをUTF-8と明示するこの設定でなぜ、どこまで安全になるか、PHPカンファレンスで実演した脆弱性デモとの対応で説明します。文字コードをやや安全に扱うphp.ini設定はなぜ安全か以下の説明は、先に紹介したプレゼン資料と対比させながらご覧ください。半端な先行バイトはUTF-8でもあり得ますが、UTF-8→UTF-8変換の過程で半端な先行バイトは除去されます。非最短形式のUTF-8もUTF-8→UTF-8変換の過程で除去されます「デモ3:5C問題によるSQLインジェクション」への対応UTF-8では原理的に5C問題は発生しません「デモ4:UTF-7によるXSS」への対応default_charsetをUTF-8に設定することで、HTTPレスポンスヘッダの文字エンコーディングが設定され、UTF-7によるXSSを防げます。実はこれについてはphp.iniだけでは防げませんが、データベース側の設定もUTF-8に統一すれば防げます。「デモ6:U+00A5によるXSS」への対応アプリケーションの内部でUTF-8を一貫して使うことにより、文字集合の変更がなくなるので、U+00A5によるXSSは発生しません。確認方法PHPカンファレンスでも紹介した「尾骶骨テスト」や「つちよしテスト」が有効です。以下の文字を入力・登録して、どのように表示されるかを調べてください。UTF-8では4バイトになるこのテストにより、文字集合の変更がどこかで起こってないかを確認することができます。データベースによってはBMP外の文字(U+10000以降の文字)に対応していないものもあるので、「つちよしテスト」を全てのアプリケーションがパスできるとは限りません。したphp.ini設定は、UTF-8→UTF-8の変換以外はバッドノウハウでもなんでもなくて、ごくまともな設定です。問題は、UTF-8→UTF-8の変換に文字エンコーディングの妥当性チェックを頼っているので、サーバーの移設などでphp.iniの設定が変更され、文字エンコーディングの自動変換がされなくなった場合に危険になることです。php.iniが変更された可能性がある場合は、phpinfoなどにより、mbstring.encoding_translationがOnになっていることを確認してください。同様に、default_charsetがUTF-8になっていることを確認してください。いう問題もあるので、本当はスクリプトで、レスポンスヘッダの文字エンコーディング指定や、文字エンコーディングのチェックをした方が頑丈なアプリケーションになります。新規開発の場合は、フレームワークの設定やカスタマイズなどで、これらの処理が行われることを確実にするとよいでしょう。講演中にhtmlspecialcharsの第3引数(文字エンコーディング指定)を必ず指定するように強調しましたが、それは今回紹介する方法でも変わりません。技術力のある方の中には、「でも入口で不正な文字エンコーディングが除去されているから、htmlspecialcharsの第3引数は指定しなくても一緒でしょ」と言う人もいると思いますが、以下のような実効性もあるのです。それは、mbstringではチェックしないが、htmlspecialcharsではチェックするという項目があるのです。UTF-8の5バイト、6バイトの形式です。これを確認するスクリプトを作りました。htmlspecialcharsに文字エンコーディングを指定しないと、そのままこのように、mbstringではUTF-8の5、6バイトの表現を認めていますが、htmlspecialcharsを通すと、5バイト以上の表現は除去されます*1。IEやFirefoxなど、現在広く使用されているブラウザはUTF-8の5、6バイトの表現を認識しないと思いますが、万一、UTF-8の5、6バイトの表現を用いた攻撃手法が見つかっても、htmlspecialcharsが削除してくれると安心です。いうこともあり得るので、htmlspecialcharsの第3引数は指定するようにしましょう。これも、フレームワークに組み込むか、ラッパー関数を用意しておけば簡便です。本当は、htmlspecialcharsのデフォルト文字エンコーディングがmbstring.internal_encodingになってくれると楽ちんなのですが、現在はISO-8859-1がデフォルトなので、こういう面倒なことになります。まとめ「文字コードをやや安全に扱うphp.ini設定」を紹介し、安全になる根拠および限界を説明しました。「これさえやっておけば、おk」となるのではなく、むしろ文字コードの問題に興味を持っていただくためのきっかけになれば幸いです。文字コードの話は、SQLインジェクションとかXSSなど脆弱性対策をした上での話ですので、php.iniだけで脆弱性対策が不要になるわけではないので、念のため。