PHP 開発において、ユーザー入力を用いたファイル操作を行う際、適切な検証処理の実装はセキュリティ上極めて重要です。特に正規表現や文字列置換関数の不適切な使用は、ディレクトリトラバーサルや任意ファイル削除といった深刻な脆弱性につながることがあります。本稿では、正規表現の検証不備および文字列置換のバイパス手法に焦点を当て、具体的なコード例を通じてそのリスクを分析します。
正規表現によるパス検証の不備
ファイル操作機能において、ユーザーから受け取ったパス文字列を正規表現でフィルタリングする際、許可されていない文字が含まれていないか厳密にチェックする必要があります。しかし、検証パターンが不十分であると、攻撃者が特殊な文字列を構築してセキュリティ対策を迂回できる可能性があります。
例えば、以下のようなコードにおいて、ファイル名に含められる字符を制限しようとしています。
<?php
$filename = $_GET['file'];
// 英数字、ドット、アンダースコア、ハイフンのみを許可する正規表現
if (preg_match('/^[a-z0-9._-]+$/i', $filename)) {
unlink('/var/www/uploads/' . $filename);
} else {
die('Invalid filename');
}
?>
この正規表現は一般的なファイル名文字を許可していますが、ディレクトリ移動を示すスラッシュ(/)やドットとスラッシュの組み合わせ(../)を直接防ぐものではありません。しかし、より問題となるのは、特定の文字列置換と組み合わされた場合です。検証ロジックがディレクトリパスの構造を考慮していない場合、相対パスを用いた攻撃が可能になります。
攻撃者は以下のようなパラメータを構築することで、意図しないファイルを削除できる可能性があります。
GET /delete.php?file=../../config.ini
正規表現がスラッシュを許可していない場合でも、後述する文字列置換の挙動を利用することで、結果的にパス traversel を実現できるケースが存在します。
CTF 問題を通じた正規表現の理解
正規表現の挙動を深く理解するために、セキュリティコンテスト(CTF)で出題されるようなコード構造を分析します。以下のコードは、複雑な正規表現チェックを経て、特定の条件を満たすと秘密情報を出力する仕組みになっています。
<?php
// secret.php
$secret_token = "FLAG{SecureRegexCheck}";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input_key = $_POST['key'];
// 印刷可能文字で 10 文字以上であること
if (!preg_match('/^[\x20-\x7E]{10,}$/', $input_key)) {
die('Format Error');
}
$pattern = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
// 異なる文字種が 5 回以上連続して出現すること
if (preg_match_all($pattern, $input_key, $matches) < 5) {
die('Complexity Error');
}
$types = ['punct', 'digit', 'upper', 'lower'];
$match_count = 0;
foreach ($types as $type) {
if (preg_match("/[[:$type:]]+/", $input_key)) {
$match_count++;
}
}
// 少なくとも 3 種類の文字種を含むこと
if ($match_count < 3) {
die('Type Error');
}
// 最終的な比較(弱比較)
if ("1024" == $input_key) {
echo $secret_token;
} else {
echo 'Access Denied';
}
}
highlight_file(__FILE__);
?>
このコードでは、PHP の正規表現字符クラス(Character Classes)の知識が問われています。主な字符クラスは以下の通りです。
| クラス名 | 意味 |
|---|---|
| alnum | 英数字 |
| alpha | 英字 |
| digit | 数字(\d と同等) |
| graph | スペースを除く印刷可能文字 |
| lower | 小文字 |
| upper | 大文字 |
| punct | 記号(英数字を除く印刷可能文字) |
| space | 空白文字 |
| word | 単語文字(\w と同等) |
コードのロジックを解析すると、入力値は長さ、連続する文字種の変化、含まれる文字種の多様性について厳格なチェックを受けます。しかし、最終的な判定では弱比較(==)が使用されており、かつ最初の長さチェックを bypass できる手法(例えば配列を渡して preg_match をエラーさせるといった手法)が存在する場合、論理矛盾を突いてフラグを取得できる可能性があります。これは、複数の検証レイヤーがあっても、最終的な比較処理や型変換の挙動を軽視すると危険であることを示しています。
実世界での事例:文字列置換のバイパス
実際の CMS フレームワークにおいて、str_replace() 関数の使用法によっては、ディレクトリトラバーサル対策が無力化されるケースがあります。ThinkPHP ベースのシステムで見られた事例を元に解説します。
以下は、テンプレートファイルを削除するコントローラーの簡略化されたコードです。
// FileManagerController.class.php
public function delete() {
$dir = I('get.dir');
$file = I('get.file');
// 危険なパス表記を削除しようとする試み
$blacklist = array('..\\', '../', './', '.\\');
$clean_dir = str_replace($blacklist, '', $dir);
$target_path = ROOT_PATH . $clean_dir . $file;
if (file_exists($target_path)) {
unlink($target_path);
echo 'Deleted';
}
}
このコードでは、str_replace() を使用して../などの文字列を空文字に置換することで、親ディレクトリへの移動を防ごうとしています。しかし、str_replace() は左から右へ一度だけ置換を行うため、入力値を工夫することでこのフィルタを迂回できます。
例えば、入力値として.....///を与えた場合、処理は以下のようになります。
- 元の文字列:
...../// ../の置換対象を探す:- 先頭の
..と続く.および/の組み合わせにより、置換後に../が復活する可能性があります。
具体的には、.....///という文字列に対して../を削除する処理が行われる際、中央部分の文字が結合されて結果的に../が生成される現象を利用します。
攻撃者は以下のようなリクエストを構築します。
GET /index.php?g=Template&m=FileManager&a=delete&dir=.....///Application/Install/&file=install.lock
このペイロードにより、フィルタリング処理をすり抜け、インストールロックファイルなど重要なファイルが削除されてしまいます。この脆弱性は、単純な文字列置換でパス正規化を行おうとすることの危険性を示しており、 realpath() 関数を用いた絶対パスの解決や、ホワイトリストによる厳格なディレクトリ制限を行うべきです。