hnwの日記

mb_check_encodingは何をチェックするのか(その3 UTF-8編)


(2009/10/05追記)「サロゲートペアに相当する3バイト表現も正しいとみなしている」という件はバグとしてPHP5.3.0から修正されているようです。id:moriyoshiさんに超感謝。


PHPのmb_check_encoding関数の調査、おそらく今回が最終回です。今回はUTF-8について調べてみました。

UTF-8

UTF-8というのはUnicodeエンコーディング形式の一つです。本当にざっくり言ってしまうと、ASCIIが1バイト、ヨーロッパ圏の文字が2バイト、漢字などが3バイトで表現されるようなエンコーディングです。


今回は、「UTF-8 - Wikipedia」を参考に、4バイトまでのビットパターンを全数調査しました。5バイト、6バイトも少し実験しました。

  • 1byte : 0xxxxxxx
  • 2byte : 110yyyyx 10xxxxxx
  • 3byte : 1110yyyy 10yxxxxx 10xxxxxx
  • 4byte : 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx
  • 5byte : 111110yy 10yyyxxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 6byte : 1111110y 10yyyyxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx


その結果、mb_check_encodingの挙動は次のようなものでした。

  • 1バイトから6バイトまで、上記のビットパターン全てを正しいと判定
    • ただし、UTF-8の冗長表現は不正とみなす。
  • 上記にあてはまらないものは全て不正とみなす


ここで言うUTF-8の冗長表現とは、本当は1バイトで表現できるのに2バイトUTF-8のビットパターンで表現するようなバイト列です。例えば、0x3C(00111100)1バイトの代わりに0xC0(11000000) 0xBC(10111100)の2バイトで表現するようなものです。最短なら2バイトで表現できるのに4バイトになっているなど、全てのパターンの冗長表現をmb_check_encodingは不正とみなします。


ところで、上記のmb_check_encodingの挙動について、2点ほど問題となりうる点に気づきました。

  • 仕様によっては不正とされている5バイト、6バイトの表現を許容している
  • サロゲートペアに相当する3バイト表現も正しいとみなしている


UTF-8のセキュリティ問題というと冗長表現の話題ばかりの印象ですが、上記の問題がセキュリティホールになりうることは無いのかな、と不安になりました。

サロゲートペアについて

サロゲートペアについてもう少し詳しく説明します。


UTF-16では、U+10000以降の文字について、0xD8 0x00から0xDF 0xFFまでの2バイト列の2組セット(=4バイト)で1文字を表します。これらは2バイト単体では不正な文字になります。実際、mb_check_encoding("\xD8\x00", "UTF-16BE")はfalseになります。


ところが、UTF-16の0xD8 0x00に相当するUTF-8のバイト列0xED 0xA0 0x80は、mb_check_encodingがtrueを返します。

$ php -r 'var_dump(mb_check_encoding("\xD8\x00", "UTF-16BE"));'
bool(false)
$ php -r 'var_dump(mb_check_encoding("\xED\xA0\x80", "UTF-8"));'
bool(true)


ついでに言うと、このバイト列はUTF-8からUTF-16には変換できますが、逆向きには変換できません。

$ php -r 'var_dump(bin2hex(mb_convert_encoding("\xED\xA0\x80", "UTF-16BE", "UTF-8")));'
string(4) "d800"
$ php -r 'var_dump(bin2hex(mb_convert_encoding("\xD8\x00", "UTF-8","UTF-16BE")));'
string(0) ""

まとめ

今回は調査結果を貼っただけで、まとめらしいまとめはありません。サロゲートペアまわりで新しいセキュリティホールが見つかったりすると楽しいな、と思ったので記事にしてみました。


とはいえ、PHPのバグを見つけたのは間違いなさそうなので、あとでPHPソースコードを読んでみます。