最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について

前の記事(Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある)で Shift_JIS ではブラウザによっては XSS が発生する可能性があることを書きました。この問題は、PHP の開発版のスナップショット(snaps.php.net)で修正されたことを確認しましたが、それ以外にも、EUC-JP や UTF-8 について修正が行われました。この件で、行動された方に感謝します。どうもありがとうございました。その後、修正内容について調べましたので、メモしておきます。他の修正点など、何か気付いた方がおられましたら、ぜひ教えてください。

この修正は、PHP 5.3.2/PHP 5.2.12 で反映されることになると思います。実際の修正内容の大半は、以下で行われました。

今後、まだ修正が行われる可能性がありますが、現時点(2009.10.16)で私が確認した変更内容は、以下の通りです。

  1. Shift_JIS
    • \xf0 - \xfc が単独で指定された場合、空文字列を返すようになった
    • Shift_JIS の先行バイト(\x81 - \x9f, \xf0 - \xfc)に続くバイト列が Shift_JIS として不正な場合、空文字列を返すようになった
  2. EUC-JP
    • EUC-JP の先行バイト(\xa1 - \xfe)に続くバイト列が EUC-JP として不正な場合、空文字列を返すようになった
  3. UTF-8

以下は検証コードです。変更されなかった点についても書いておきました。

1. Shift_JIS

(1) \xf0 - \xfc が単独で指定された場合、空文字列を返すようになった
<?php
var_dump( bin2hex( htmlspecialchars( "\xf0", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf1", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf2", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf3", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf4", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf5", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf6", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf7", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf8", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xf9", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xfa", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xfb", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xfc", ENT_QUOTES, "Shift_JIS" ) ) );
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(2) "f0"
string(2) "f1"
string(2) "f2"
string(2) "f3"
string(2) "f4"
string(2) "f5"
string(2) "f6"
string(2) "f7"
string(2) "f8"
string(2) "f9"
string(2) "fa"
string(2) "fb"
string(2) "fc"
修正後(最新のスナップショット)の結果です。これらの文字列は出力されないようになりました。
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
(2) Shift_JIS の先行バイト(\x81 - \x9f, \xf0 - \xfc)に続く1バイトが Shift_JIS として不正な場合、空文字列を返すようになった
<?php
var_dump( bin2hex( htmlspecialchars( "\x81\x3e", ENT_QUOTES, "Shift_JIS" ) ) );  // \x3e は '>'
var_dump( bin2hex( htmlspecialchars( "\x9f\x3e", ENT_QUOTES, "Shift_JIS" ) ) );  // \x3e は '>'
var_dump( bin2hex( htmlspecialchars( "\xf0\x3e", ENT_QUOTES, "Shift_JIS" ) ) );  // \x3e は '>'
var_dump( bin2hex( htmlspecialchars( "\xfc\x3e", ENT_QUOTES, "Shift_JIS" ) ) );  // \x3e は '>'
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "812667743b"
string(10) "9f2667743b"
string(10) "f02667743b"
string(10) "fc2667743b"
修正後(最新のスナップショット)の結果です。これらの文字は出力されないようになりました。
string(0) ""
string(0) ""
string(0) ""
string(0) ""
(3) \x80, \xa0, \xfd, \xfe, \xff については、そのまま出力される
<?php
var_dump( bin2hex( htmlspecialchars( "\x80", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xa0", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xfd", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xfe", ENT_QUOTES, "Shift_JIS" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xff", ENT_QUOTES, "Shift_JIS" ) ) );
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(2) "80"
string(2) "a0"
string(2) "fd"
string(2) "fe"
string(2) "ff"
修正後(最新のスナップショット)の結果です。これらの文字列については変更ありません。
string(2) "80"
string(2) "a0"
string(2) "fd"
string(2) "fe"
string(2) "ff"

2. EUC-JP

(1) EUC-JP の先行バイト(\x8e, \x8f, \xa1 - \xfe)に続くバイト列が EUC-JP として不正な場合、空文字列を返すようになった
<?php
var_dump( bin2hex( htmlspecialchars( "\x8e\x3e",     ENT_QUOTES, "EUC-JP" ) ) );  // \x3e は '>'
var_dump( bin2hex( htmlspecialchars( "\x8f\x3e\x3c", ENT_QUOTES, "EUC-JP" ) ) );  // \x3e は '>', \x3c は '<'
var_dump( bin2hex( htmlspecialchars( "\xa1\x3e",     ENT_QUOTES, "EUC-JP" ) ) );  // \x3e は '>'
var_dump( bin2hex( htmlspecialchars( "\xfe\x3e",     ENT_QUOTES, "EUC-JP" ) ) );  // \x3e は '>'
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "8e2667743b"
string(18) "8f2667743b266c743b"
string(10) "a12667743b"
string(10) "fe2667743b"
修正後(最新のスナップショット)の結果です。これらの文字列は出力されないようになりました。
string(0) ""
string(0) ""
string(0) ""
string(0) ""
(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"

3. UTF-8

(1) 冗長な表現が使用された場合、空文字列を返すようになった
<?php
var_dump( bin2hex( htmlspecialchars( "\x3c",                     ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の冗長でない表現)
var_dump( bin2hex( htmlspecialchars( "\xc0\xbc",                 ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の2バイト表現)
var_dump( bin2hex( htmlspecialchars( "\xe0\x80\xbc",             ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の3バイト表現)
var_dump( bin2hex( htmlspecialchars( "\xf0\x80\x80\xbc",         ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の4バイト表現)
var_dump( bin2hex( htmlspecialchars( "\xf8\x80\x80\x80\xbc",     ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の5バイト表現)
var_dump( bin2hex( htmlspecialchars( "\xfc\x80\x80\x80\x80\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の6バイト表現)
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(8) "266c743b"
string(8) "266c743b"
string(8) "266c743b"
string(8) "266c743b"
string(8) "266c743b"
string(8) "266c743b"
修正後(最新のスナップショット)の結果です。冗長な表現の場合は出力されなくなりました。
string(8) "266c743b"
string(0) ""
string(0) ""
string(0) ""
string(0) ""
string(0) ""
(2) UTF-8 の 5, 6 バイト表現が指定された場合、空文字列を返すようになった
<?php
var_dump( bin2hex( htmlspecialchars( "\xf8\xbf\xbf\xbf\xbf",     ENT_QUOTES, "UTF-8" ) ) ); // U+3FFFFFF  (UTF-8 では 5バイトで表現)
var_dump( bin2hex( htmlspecialchars( "\xfc\xbf\xbf\xbf\xbf\xbf", ENT_QUOTES, "UTF-8" ) ) ); // U+7FFFFFFF (UTF-8 では 6バイトで表現)
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "f8bfbfbfbf"
string(12) "fcbfbfbfbfbf"
修正後(最新のスナップショット)の結果です。5バイト、6バイトの表現は出力されなくなりました。
string(0) ""
string(0) ""
(3) U+10000 以降の一部の文字が不正に変換される問題が修正された
徳丸さんが書かれた htmlspecialchars/htmlentitiesはBMP外の文字を正しく扱えない を参照ください。
(4) サロゲートペア(U+D800 - U+DFFF)の領域は、そのまま出力される
(4) サロゲートペア(U+D800 - U+DFFF)の領域を指定すると、空文字列を返すようになった(2009.12.08)
<?php
var_dump( bin2hex( htmlspecialchars( "\xed\xa0\x80", ENT_QUOTES, "UTF-8" ) ) ); // U+D800
var_dump( bin2hex( htmlspecialchars( "\xed\xbf\xbf", ENT_QUOTES, "UTF-8" ) ) ); // U+DFFF
PHP 5.3.0 / PHP 5.2.11 の結果です。
string(6) "eda080"
string(6) "edbfbf"
修正後(2009.12.06 以前のスナップショット)の結果です。これらの文字列についてはそのまま出力されます。
string(6) "eda080"
string(6) "edbfbf"
2009.12.07 以降のスナップショットでは、これらの文字列については出力されなくなりました。
string(0) ""
string(0) ""

2009.12.08 追記
id:moriyoshi さんからサロゲートペアの問題も回避するようになったというコメントをいただきました。2009.12.07 以降のスナップショットを使用すれば、サロゲートペアの領域は出力されなくなります。