<?php header('Content-Type: text/plain; charset=UTF-8'); $file_utf8 = basename($_GET['file']); $file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8'); $path = './data/' . $file_sjis; var_dump($path); readfile($path);
しかし、ディレクトリトラバーサル対策は十分でなく、このスクリプトには脆弱性があります。下図は、ディレクトリトラバーサル攻撃により、このスクリプトの中身を読み出しているところです。
以下、このスクリプトの問題点、さらにはbasename関数を用いる際の注意点について説明します。
basename関数はマルチバイト対応していないという誤解
ネットの記事を見ていますと、basename関数はマルチバイト対応していないという主張をよく見かけます。例えば、下記の記事。basename関数はパスからディレクトリ情報を削除してベース名(「test.txt」など)を取得するための関数ですが、PHP5(使用バージョン:PHP 5.3.1)ではパスに日本語が含まれていると失敗します。このスクリプトをWindowsサーバー上で動かすと、確かに下図のようにファイル名が化けます。下図ではファイル名の16進数表記を参考のためつけています。「テ」の先頭1バイト0x83が欠落しています。
▼失敗するbasename関数
<?php $a = "/dir/テスト.txt"; echo basename($a); ?>ファイル名などに日本語が含まれるパスでbasename関数が失敗するバグより引用
localeに注意
しかし、このスクリプトはbasename関数の使い方に問題があります。basenameのマニュアルには以下の注意書きがあります。注意:これに従い、先のスクリプトを修正してみます。
basename() はロケールに依存します。 マルチバイト文字を含むパスで正しい結果を得るには、それと一致するロケールを setlocale() で設定しておかなければなりません。
こうすると、下図のように正しい結果が得られます。マルチバイト環境でbasename関数を利用するにはlocaleの設定が必要であることが分かります。<?php $a = "/dir/テスト.txt"; setlocale(LC_CTYPE, 'Japanese_Japan.932'); // 追加 echo basename($a);
冒頭の脆弱なスクリプトへの攻撃法
ここで、冒頭の脆弱なサンプルがなぜ問題かを説明します。このスクリプトにもlocaleの指定がありませんが、それが根本原因ではありません。まず、攻撃に用いる文字列を示します。以下のクエリ文字列により攻撃が可能です。
..%C2%A5vulbasename.php%C2%A5はUnicodeの円記号(U+00A5)のUTF-8表記をパーセントエンコードしたものです。これをデコードすると以下の通りです。
..¥vulbasename.php「¥」は前述のとおり円記号U+00A5です。さらに、これをcp932に変換することになりますが、この際に「¥」U+00A5がバックスラッシュ「\」0x5Cに変換されます。
..\vulbasename.phpこのため、組み立てられるパス名は以下の通りとなります。これは典型的なディレクトリトラバーサル攻撃の文字列ですね。
./data/..\vulbasename.php
文字エンコーディング変換するタイミングに注意
basename関数を通しているにもかかわらず攻撃が成立してしまう理由は、以下の通りです。- 円記号U+00A5は、basenameの処理対象の文字ではない
- basenameの処理の後に、文字エンコーディング変換によりU+00A5が0x5Cに変化する
- まず文字エンコーディングを変換する
- その後にbasename関数を通す
このスクリプトに対して先の攻撃をかけても、以下のようにスクリプトの中身は表示されません。$file_utf8 = $_GET['file']; $file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8'); // 文字エンコーディング変換 setlocale(LC_CTYPE, 'Japanese_Japan.932'); // locale設定 $file = basename($file_sjis); $path = './data/' . $file;
basenameはファイル名として妥当な文字種のチェックをしない
basename関数のソースを読むとすぐ分かりますが、basename関数がチェックするのはスラッシュ(全プラットフォーム共通)、バックスラッシュとコロン(Windows等)のみです。特にWindowsの場合ファイル名に使える文字に制約がありますが、basenameはそのチェックはしません。長さのチェックもしません。このため、ファイル名を外部から受け取る場合、ファイル名の仕様として以下を決めておくとよいでしょう。
- ファイル名に用いる文字の種類
- ファイル名を表現する文字エンコーディング
- ファイル名の長さの最小値・最大値
まとめ
PHPのbasename関数を用いる上での注意点を説明しました。まとめると以下のようになります。- 文字エンコーディングの変換はbasenameを通す前に行うこと
- basename関数を呼ぶ前にlocaleを設定すること
- ファイル名の仕様を決める
- ファイル名が仕様を満たすかどうかバリデーションにより確認する
0 件のコメント:
コメントを投稿