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) ""