htmlspecialchars()/htmlentities()について

id:t_komuraさんの、最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容についてを読みました。

見ていて気になったことが1つあります。

2. EUC-JP

…(省略)…

 (2) \x80 - \x8d, \x90 - \xa0, \xff については、そのまま出力される

<?php
var_dump( bin2hex( htmlspecialchars( "\x80", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\x8d", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\x90", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xa0", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xff", ENT_QUOTES, "EUC-JP" ) ) );

PHP 5.3.0 / PHP 5.2.11 の結果です。

string(2) "80"
string(2) "8d"
string(2) "90"
string(2) "a0"
string(2) "ff"

修正後(最新のスナップショット)の結果です。これらの文字列については変更ありません。

string(2) "80"
string(2) "8d"
string(2) "90"
string(2) "a0"
string(2) "ff"

この挙動は、一部のブラウザにおいて問題になると思われます。

というのも、実は一部のブラウザにおいては、本来のEUC-JPの先行バイトである「\x8e, \x8f, \xa1 - \xfe」以外のバイトでも、後続の文字を食いつぶす現象が発生するからです。

少し古い情報ですが、Bypassing script filters via variable-width encodingsに、ブラウザごとに後続の文字を食いつぶすバイトが書かれています。

 +-----------+-----------+-----------+-----------+
 |           | IE        | FF        | OP        |
 +-----------+-----------+-----------+-----------+
 | UTF-8     | 0xC0-0xFF | none      | none      |
 +-----------+-----------+-----------+-----------+
 | GB2312    | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | GB18030   | none      | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | BIG5      | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | EUC-KR    | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | EUC-JP    | 0x81-0x8D | 0x8F      | 0x8E      |
 |           | 0x8F-0x9F |           | 0x8F      |
 |           | 0xA1-0xFE |           | 0xA1-0xFE |
 +-----------+-----------+-----------+-----------+
 | SHIFT_JIS | 0x81-0x9F | 0x81-0x9F | 0x81-0x9F |
 |           | 0xE0-0xFC | 0xE0-0xFC | 0xE0-0xFC |
 +-----------+-----------+-----------+-----------+

上表の赤字箇所をみると、IE(IE6)はEUC-JPの「\x8e, \x8f, \xa1 - \xfe」以外のバイトである「\x81 - \x8d, \x90 - \x9f」でも後続の文字を食いつぶすことが分かります。上の表は2006年の情報で少々古いのですが、私が3〜4ヶ月前にIE6/IE7で試した時も、上表と同じ結果になりました。

ですので、IEEUC-JPの場合、id:t_komuraさんの日記で説明されている最新版のhtmlspecialchars()でエスケープを行ったとしても、後続の文字を食いつぶす攻撃を防げない(場合がある)、ということになります。

****

ところで、上の現象が起こるのはIEだけです。IEでそのような現象が起こる理由は、本題ではないので割愛します。また、IE側が異常(脆弱)であるという捉え方もありますが、ここではひとまずIE側の話はおいておきます。

htmlspecialchars()側としてどうなのかを考えると、どうせ文字チェックをするのであれば「\x8e, \x8f, \xa1 - \xfe」だけをはじくという、ある種のブラックリスト的な方法ではなく、いっそのこと「EUC-JPとして正常なシーケンスしか出力しない」というホワイトリスト的なアプローチをとることができないものだろうかと思います。

ホワイトリスト的な方法にもいくつかのレベルがありますが、比較的シンプルなのは、以下のようなラフな正規表現Perl風)にマッチしないものをinvalidだとみなす方法です。

\A([\x00-\x7f]|(\x8F?[\xA1-\xFE]|\x8E)[\xA1-\xFE])*\Z

さらにホワイトリスト的なアプローチを突き詰めると、文字集合の話が出てきます。例えば、機種依存文字やユーザ定義文字(絵文字など)をどうするか、あるいはIEが解釈できない補助漢字などの3バイト文字をどうするかのような話です。

理想を言えば、htmlspecialchars()が、通常のEUC-JPに加えて、一般的に使われる亜種コード(eucjp-win, CP51932)もサポートした上で、文字集合のチェックまで実施するのが望ましいと思います(PHPマニュアルによると、現状のhtmlspecialchars()は、eucjp-winやCP51932といった文字コードをサポートしていません)。