2015年2月13日金曜日

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

昨日のエントリにて、PHPのbasename関数はマルチバイト文字を扱えることを説明しましたが、このブログの読者であれば、きっとbasename関数は不正な文字エンコーディングについてどの程度チェックするのかという疑問が生じたことでしょう(きっぱり)。実はbasename自体は、不正な文字エンコーディングをチェックせず、垂れ流してしまいます。その理由をbasenameのソースコードで確認してみましょう。以下は、basename関数の実装の一部です。
// ext/standard/string.c
// php_basenmae()
while (cnt > 0) {
  inc_len = (*c == '\0' ? 1: php_mblen(c, cnt));

  switch (inc_len) {
    case -2:
    case -1:
      inc_len = 1;
      php_ignore_value(php_mblen(NULL, 0));
      break;
    case 0:
      goto quit_loop;
php_mblen関数はmblen(3)のラッパーです。mblen関数は文字列の先頭文字のバイト数を返す関数で、先頭の文字が不正なエンコーディングの場合 -1 を返します。上記のソースでは、mblenが-1を返した場合は、inc_len=1として正常な1バイト文字と見なして処理を継続しています。
一方以下は、シェル呼び出しのエスケープを行うescapeshellarg関数の実装ですが…
// ext/standard/exec.c
// php_escape_shell_arg()
for (x = 0; x < l; x++) {
  int mb_len = php_mblen(str + x, (l - x));

  /* skip non-valid multibyte characters */
  if (mb_len < 0) {
    continue;
  } else if (mb_len > 1) {
    memcpy(cmd + y, str + x, mb_len);
    y += mb_len;
    x += mb_len - 1;
    continue;
  }
  switch (str[x]) {
    // 文字に応じた処理
コメントにあるように、不正な多バイト文字はスキップする、すなわち除去(フィルタリング)されます。

同じmblen関数を使っていても、basename関数とescapeshellarg関数では、不正な文字エンコーディングに対する対処方法が違っています。ともかく、basename関数は、不正な文字エンコーディングをエラーとせず、結果の中に含めてしまいます。

不正な文字エンコーディングの影響の考察(Windowsの場合)

basename関数が不正な文字エンコーディングのチェックをしていないことによるセキュリティ上の影響はないでしょうか。具体的に確認するために、まずWindowsの場合について検討します。すなわち、ファイル名が Shift_JIS でエンコーディングされているとします。
ディレクトリトラバーサル攻撃の攻撃パターンとしては、絶対パスによるものと相対パスによるものがありますが、絶対パスの場合ファイル名の冒頭に / や \ が来る必要があり、これは「不正な文字エンコーディング」にはなりえません。相対パスの方は、 ../ などシングルバイトの文字が連続して続く必要がありますが、/や\が単独の場合は単に除去され、その前にマルチバイト文字の先行バイトがある場合でも、前述の理由から先行バイトは除去されません(..■/ の形になる)。.その後 / までが除去される可能性が高いですが、仮に除去されない状況でも、..■/ と余計な文字がはさまっているため、攻撃パターンを形成しないと思われます。

不正な文字エンコーディングの影響の考察(Linuxの場合)

次にLinuxの場合について考えます。文字エンコーディングは UTF-8 とします。この場合、basename関数はUTF-8の冗長表現を通してしまいます。
これを検証するためのスクリプトを以下に示します。\xC0\xAF は / をUTF-8の2バイト表現にしたものです。
<?php
  setlocale(LC_CTYPE, 'ja_JP.UTF-8');
  echo bin2hex(basename("..\xC0\xAFaaa")), PHP_EOL;
出力は下記となります。c0afがそのまま出力されていることがわかります。
2e2ec0af616161
UTF-8の冗長表現が許可されるというと、NimdaワームやTomcatの脆弱性CVE-2008-2938を思い出す人も多いと思います。「それは問題ではないか」と思うところですが、現実には問題になるケースはほとんどないと考えられます。

その理由は下記のとおりです。
  • Linuxで使われるファイルシステムでは、\xC0\xAF等はそのままのバイト列としてファイル名に使われ、ディレクトリ区切子とは認識されない
そこで次の可能性は、UTF-8の冗長表現表現としてbasenameをパスした文字列が、その後文字エンコーディング変換されてシングルバイトの / に変換されることですが、
  • そもそもbasenameの後に文字エンコーディング変換をすることはよろしくない(参考
  • PHPで文字エンコーディング変換に使用される mb_convert_encodingとiconvはどちらもUTF-8の冗長表現をエラーにするかフィルタリングするので、攻撃文字列は形成されない
ということで、basename関数が冗長なUTF-8エンコーディングを許容しても、実害が出るケースはほとんどないと考えられます。実害があるとすると、独自実装の脆弱性のある文字エンコーディング変換機能を利用している場合ですが、その場合でも文字エンコーディング変換後にbasename関数を通すという正しい手順を踏んでいれば、問題は顕在化しません。

緩和策

basename関数は不正な文字エンコーディングを許容することが分かりましたが、これによる実害はほとんどなさそうです。ただし、外部から与えられたファイル名で新規にファイルを作成する場合は、変なファイル名のファイルができてしまいます。
いずれにせよ、以下をアプリケーションの仕様として決めておくとよいでしょう(再掲)。
  • ファイル名に用いる文字の種類
  • ファイル名を表現する文字エンコーディング
  • ファイル名の長さの最小値・最大値
そして、以下を推奨します。
  • 文字エンコーディングの変換はbasenameを通す前に行うこと
  • basename関数を呼ぶ前にlocaleを設定すること
  • ファイル名の仕様を決める
  • ファイル名が(文字エンコーディングを含め)仕様を満たすかどうかバリデーションにより確認する

まとめ

PHPのbasename関数が不正な文字エンコーディングを許容してしまうことを説明しました。この問題は一応bug#68773として報告済みですが、報告から1ヶ月以上たってもアサインもされていませんので、少なくともすぐに修正される可能性は薄そうです。幸い実害もあまりなさそうですが、念のためバリデーションにより文字エンコーディングのチェックをしておくと安心です。
PHPの文字列は単なるバイト列ですので、一般論として、アプリケーションの開始時に文字エンコーディングのチェックをしておくことにより、不正な文字エンコーディングの文字を弾いておくことをお勧めします。アプリケーションの前提条件を満たしていない入力を予め除外しておくことでアプリケーションの安定動作のために寄与します。
また、basenameの現在の実装は少々いただけないと考えます。せっかくmblenが不正な文字エンコーディングをチェックして -1 を返しているのに、そのエラーを「なかったことに」しているからです。一方、シェルのエスケープを行うescapeshellargの方は不正な文字エンコーディングをフィルタリングしているわけで、同じPHPの中で一貫性のない挙動というのも(PHPらしいといえばそれまでですが)よくないように感じました。

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ