プライバシーポリシー

2010年9月27日月曜日

文字コードに起因する脆弱性を防ぐ「やや安全な」php.ini設定

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2010年9月27日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

PHPカンファレンス2010にて「文字コードに起因する脆弱性とその対策」というタイトルで喋らせていただきました。プレゼンテーション資料をPDF形式slideshare.netで公開しています。

文字コードのセキュリティというと、ややこしいイメージが強くて、スピーカーの前夜祭でも「聴衆の半分は置いてきぼりになるかもね」みたいな話をしていたのですが、意外にも「分かりやすかった」等の好意的な反応をtwitter等でいただき、驚くと共に喜んでいます。土曜にPHPカンファレンスに来られるような方は意識が高いというのもあるのでしょうね。

さて、その場で少し触れた「文字コードをやや安全に扱う方法」を紹介したいと思います。
実はこの方法を紹介するかどうかは悩みどころでして、完全に安全になるならともかく、「これで全部おk」みたいに開発者が文字コードのセキュリティに無関心になる方法に働くといやだなと思うわけです。しかし、ネットには、既に根拠の不明確なバッドノウハウであふれているわけで、少しマシなバッドノウハウを流通させることで、改善になるのではないかと思い、公開に踏み切りました。しかし、専門家として、どこまで安全にできるかとか、安全になる根拠は示したいと思います。

文字コードをやや安全に扱うphp.ini設定

まずは結論を示します。php.iniに(あるいは.htaccessで)以下を設定します。
;; 出力バッファリングを無効にする (追記:文字エンコーディングの変換をしなければ、On でもいいです)
output_buffering      = Off

;; HTTPレスポンスの文字エンコーディングを設定
default_charset       = UTF-8

;; デフォルトの言語を日本語にする
mbstring.language = Japanese

;; HTTP 入力変換を有効にする
mbstring.encoding_translation = On

;; HTTP 入力エンコーディング変換を UTF-8 に設定(UTF-8→UTF-8の変換)
mbstring.http_input   = UTF-8

;; HTTPレスポンスは変換しない
mbstring.http_output  = pass

;; 内部エンコーディングを UTF-8 に設定
mbstring.internal_encoding = UTF-8    

;; 無効な文字は「?」に
mbstring.substitute_character = "?"
php.iniで指定できない条件として以下も必須です。
htmlspecialcharsの第3引数に必ず'UTF-8'を指定する

PHPのソースはUTF-8でセーブする

データベースの接続・テーブル・データベースの文字エンコーディングは全てUTF-8で統一する
この設定の要点を箇条書きで説明します。
  • Webアプリケーションの入口から出口まですべてUTF-8で統一する
  • 入力時にUTF-8→UTF-8の変換をすることで、不正な文字エンコーディングを除去する
  • HTTPレスポンスの文字エンコーディングをUTF-8と明示する
この設定でなぜ、どこまで安全になるか、PHPカンファレンスで実演した脆弱性デモとの対応で説明します。

文字コードをやや安全に扱うphp.ini設定はなぜ安全か

以下の説明は、先に紹介したプレゼン資料と対比させながらご覧ください。
「デモ1:半端な先行バイトによるXSS」への対応
半端な先行バイトはUTF-8でもあり得ますが、UTF-8→UTF-8変換の過程で半端な先行バイトは除去されます。
「デモ2: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を防げます。
「デモ5:U+00A5によるSQLインジェクション」への対応
実はこれについてはphp.iniだけでは防げませんが、データベース側の設定もUTF-8に統一すれば防げます。
「デモ6:U+00A5によるXSS」への対応
アプリケーションの内部でUTF-8を一貫して使うことにより、文字集合の変更がなくなるので、U+00A5によるXSSは発生しません。

確認方法

PHPカンファレンスでも紹介した「尾骶骨テスト」や「つちよしテスト」が有効です。以下の文字を入力・登録して、どのように表示されるかを調べてください。



  • ¥(U+00A5) バックスラッシュに変換されないか
  • 骶(U+9AB6) JIS X 0208にない文字
  • 𠮷(U+20BB7) BMP外の文字 UTF-8では4バイトになる

  • このテストにより、文字集合の変更がどこかで起こってないかを確認することができます。データベースによってはBMP外の文字(U+10000以降の文字)に対応していないものもあるので、「つちよしテスト」を全てのアプリケーションがパスできるとは限りません。逆に、U+00A5がバックスラッシュ(0x5C)に化けるアプリケーションには潜在的な危険性があります。
     

    注意事項

    今回紹介した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バイトの形式です。これを確認するスクリプトを作りました。
    <?php
      $u8_6 = "\xFC\x84\x80\x80\x80\x80"; // 6バイト形式のUTF-8
      var_dump(mb_check_encoding($u8_6, 'UTF-8'));
      echo bin2hex(mb_convert_encoding($u8_6, 'UTF-8', 'UTF-8')) . "\n";
      var_dump(htmlspecialchars($u8_6, ENT_QUOTES, 'UTF-8'));
      echo bin2hex(htmlspecialchars($u8_6, ENT_QUOTES));
    
    【実行結果】
    bool(true)         # checkは通っている
    fc8480808080       # UTF-8→UTF-8の変換後もそのまま
    string(0) ""       # htmlspecialcharsにUTF-8を指定すると削除される
    fc8480808080       # 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だけで脆弱性対策が不要になるわけではないので、念のため。

    *1 この問題については、id:t_komuraさんの素晴らしいブログエントリ「最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について」に詳しい分析があります