2015年2月23日月曜日

PHPのmb_ereg関数群は不正な文字エンコーディングをチェックしない

PHPのbasename関数には、マルチバイトに対応していないという誤解(実際にはロケールの設定をすればマルチバイトでも使える)があったり、不正な文字エンコーディングをチェックしないという課題があったりで、イマイチだなーと思っている方も多いと思います。
そういう方々が、preg_replace(u修飾子つき)やmb_ereg_replaceを用いて代替関数を作成している解説も見かけますが、それではこれら正規表現関数は不正な文字エンコーディングをチェックしているのだろうかという疑問が生じます。
ざっと調べたところ、以下の様な状況のようです。
  • preg_replace : 不正な文字エンコーディングをチェックしている
  • mb_ereg_replcae : 不正な文字エンコーディングをチェックしていない
ここでは、mb_ereg_replaceが不正な文字エンコーディングをチェックしない状況と、その影響について報告します。

不正な文字エンコーディングとは

マルチバイトの文字を表現するエンコーディング(Shift_JIS、EUC-JP、UTF-8など)には、バイト列の並びのルールが決まっています。例えば、Shift_JISの2バイト目としてありえるバイト値とあり得ないバイト値があります。UTF-8の場合は、先頭バイトで文字のバイト数が決まり、2バイト目以降に使えるバイトは 0x80 ~ 0xBF と決まっています(参考)。
これらの決まりに従わないバイト列は、「不正な文字エンコーディング」ということになります。また、UTF-8には「非最短形式」というものがあり、禁止されているので、これも不正な文字エンコーディングの一種です。

mb_ereg_replaceは不正な文字エンコーディングをどう扱うか?

ここでは、UTF-8の場合を例として、mb_ereg_replaceが不正な文字エンコーディングのデータをどのように処理するかを見てみます。
まず、不正なデータとして以下を例に取ります。
$a = "\xFC../..";  // 6バイト
このデータは以下の意味でUTF-8として不正です。
  • 0xFCは、かつてUTF-8の6バイト形式として定義されていたが、現在は5バイト以上の形式は禁止されている
  • 2バイト目以降が 0x80~0xBF の範囲にない
PoCは以下の通りです。
<?php
  mb_regex_encoding('UTF-8');
  $a = "\xFC../..";
  $s = mb_ereg_replace('[\./]', '', $a);  // . と / をすべて取り除く
  echo bin2hex($s) . "\n";
結果は以下の通り。
fc2e2e2f2e2e
先頭の0xFCの影響を受けて、2バイト目以降の . や / は取り除かれていません。ここから以下のことが分かります。
  • mb_ereg_replaceはUTF-8の6バイト形式を許容している
  • mb_ereg_replaceはUTF-8の2バイト目以降のバイト値の範囲をチェックしていない
この結果は私には衝撃的だったのですが、ディレクトリトラバーサル脆弱となる現実的なシナリオは作れませんでした。腕自慢の方は挑戦してみてください。

脆弱となる例

ディレクトリトラバーサルの例は作れませんでしたので、XSSならどうだろうと思い、ちょっと人工的ですが、不正な文字エンコーディングによるXSS脆弱性の例を考えてみました。以下のスクリプトはmb_ereg_replaceを使った「手作りの」HTMLエスケープ関数を使っています。
<?php
  header('Content-Type: text/html; charset=UTF-8');
  function mb_htmlescape($s) {
    mb_regex_encoding('UTF-8');
    $s = mb_ereg_replace('&', '&amp;', $s);
    $s = mb_ereg_replace('<', '&lt;', $s);
    $s = mb_ereg_replace('>', '&gt;', $s);
    $s = mb_ereg_replace('"', '&quot;', $s);
    return $s;
  }
?>
<html><body>
<?php echo mb_htmlescape($_GET[x]); ?>
</body></html>
このスクリプトに対して、x=<script>alert(1);</script> というクエリ文字列を与えると以下のように正しくエスケープ処理が行われています。


しかし、以下のクエリ文字列だと、JavaScriptが起動されてしまいます。
x=%C2<script+%C2>alert(1);//%C2</script+%C2>

%C2はUTF-8の2バイト形式の1バイト目になるバイト値です。これを■で表現すると、入力文字列は下記の通りです。
■<script ■>alert(1);//■</script ■>
そして、この文字列は、先の手作りエスケープ関数を素通りしてそのままブラウザに送られます。
ブラウザ側では、この■<等が「不正なUTF-8文字」と認識して、■と < という別々の文字として扱われます。この「不正な文字に対する扱いの差異」が脆弱性の原因です。

対策

以下の対策を推奨します。
  • エスケープ関数等セキュリティ目的の処理は極力自作しない(htmlspecialchars関数では上記の問題は起きない)
  • 各入力値についてmb_check_encoding関数により文字エンコーディングの妥当性チェックを行う

まとめ

PHPのhtmlspecialcharsはかつていけてなかった(参考)とか、basename関数は今もいけてないなどの理由でこれらの関数を自作したくなる衝動にかられる場合がありますが、中途半端に自作するとかえって危険になる場合があります。PHPの主要な関数群は色々ディスられながら改良され、安全になってきた歴史がありますので、よほど明確な理由がない限りはPHPで提供されているものを使うほうがよいと考えます。
また、PHPが提供する関数群には文字エンコーディングのチェックを厳密に行うもの(mb_check_encoding、htmlspecialchars等)と、チェックをあまりしないもの(mb_strlen、basename、mb_ereg等)がありますので、入力値のバリデーション時にmb_check_encodingで文字エンコーディングの妥当性を確認しておくとよいでしょう。

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ