PHP実行拒否リスト

PHP_DENY_PATHS は、設定されたglobパターンにマッチする .php ファイルの直接実行をブロックします。これは、レガシーPHPアプリケーションで繰り返し発生する脆弱性のクラスを対象としています。攻撃者は書き込み可能な公開ディレクトリ(/uploads/cache、画像リサイズ用の一時ディレクトリなど)にPHPファイルをアップロードし、直接URIでそれに到達してコード実行を得ようとします。

このチェックはあらゆるディスクI/Oの前に実行されるため、拒否されたパスはファイルがディスク上に存在するかどうかにかかわらず同じレスポンスを返します。攻撃者がアップロードディレクトリを探る際に利用できる存在オラクルは存在しません。

適用される場合

直接マッピングモード、すなわちURIがディスク上の .php ファイルに直接解決されるモードです。

ルーティングモード PHP_DENY_PATHS は尊重されるか?
従来型(ENTRY_FILE なし) はい
SPA(ENTRY_FILE=index.html はい — SPAは既存の .php ファイルを直接実行するため、拒否リストが適用されます
フレームワーク(ENTRY_FILE=index.php いいえ — 警告が出て無視されます
ワーカー(WORKER_MODE_ENABLED=true いいえ — 警告が出て無視されます

フレームワークモードでは、すべてのリクエストがフロントコントローラーに書き換えられ、任意の .php ファイルが直接実行されることは決してありません。拒否リストは .php で終わるアプリケーションのルートを壊すだけです。ワーカーモードでは、静的でないすべてのリクエストがワーカースクリプトにディスパッチされるため、拒否リストが拒否すべきものは何もありません。いずれかのモードで PHP_DENY_PATHS を設定すると、起動時に警告が出てチェックが無効化されます。

拒否リストは間接的に到達されるスクリプトもカバーします。ディレクトリインデックスの検索を通じて uploads/index.php に解決される /uploads/ へのリクエストは、uploads/** がリストに載っている場合に拒否されます。パターンとマッチングされるのはリクエストURIだけでなく、解決されたスクリプトパスです。このような拒否の場合、OXPHP_DENIED_PATH はサニタイズ済みのリクエストURIを末尾のスラッシュなしで保持します(/uploads//uploads として報告されます)。

設定

bash
# Comma-separated glob patterns PHP_DENY_PATHS="/uploads/**,/cache/**,/tmp/**" # What to return on a match (default: 404) PHP_DENY_FALLBACK="403"

/uploads/shell.php へのリクエストは、ディスクに触れることなく403を返すようになります。/uploads/image.png へのリクエストは通常どおり配信されます。拒否リストは .php 実行のみに影響し、静的ファイルの配信には決して影響しません。

パターン構文

パターンは、globset 構文を使用して、サニタイズ済みのURI(.. セグメントとパーセントエンコードによるバイパスがすでに解決されたリクエストパス)とマッチングされます。各パターンの先頭の / は省略可能です。/uploads/**uploads/** は等価です。

パターン マッチする マッチしない
/uploads/** /uploads/x.php/uploads/a/b/c.php/uploads/shell.php/extra /uploads.php/public/uploads/x.php
/files/*.php /files/x.php /files/sub/x.php(単一の */ をまたがない)
/admin/legacy.php /admin/legacy.php /admin/legacy.php/x(PATH_INFO はカバーされない — 下記参照)
/admin/legacy.php{,/**} /admin/legacy.php/admin/legacy.php/x /admin/other.php
/**/wp-config.php /wp-config.php/site/wp-config.php /wp-config.txt

複数のパターンはORで結合されます。リクエストはいずれかのパターンにマッチすれば拒否リストにマッチします。

単一ファイル対ディレクトリ

どちらも機能します。/uploads/** はサブツリー全体をブロックし、/admin/legacy.php は1つの特定のスクリプトをブロックします。単一のレガシーなエントリーポイントとその PATH_INFO 呼び出し(/admin/legacy.php/foo)の両方をブロックするには、ブレース形式 /admin/legacy.php{,/**} を使用します。

大文字・小文字の区別

マッチングは大文字・小文字を区別します。大文字・小文字を区別しないファイルシステム(デフォルトのmacOS HFS+/APFS、デフォルトのWindows NTFS、casefold 付きのext4)では、/uploads/Shell.PHP へのリクエストは /uploads/**/*.php というパターンをバイパスしてしまいます。そのようなファイルシステムから配信する場合は、/uploads/**(すべての拡張子にマッチする)のような広範なディレクトリパターンを使用するか、書き込み時にアップロードを小文字に正規化してください。

フォールバックモード

PHP_DENY_FALLBACK は、マッチ時に返される内容を制御します。

HTTPステータス

400599 の範囲の任意の値(デフォルトは 404)。カスタムHTMLボディのために ERROR_PAGES_DIR と組み合わせられます。

bash
PHP_DENY_PATHS="/uploads/**" PHP_DENY_FALLBACK="403" ERROR_PAGES_DIR="/var/www/errors" # serves errors/403.html

PHPスクリプト

DOCUMENT_ROOT 内のフォールバックスクリプトへの / で始まるURIパスです。

bash
PHP_DENY_PATHS="/uploads/**" PHP_DENY_FALLBACK="/_security/denied.php"

このスクリプトは起動時に検証されます。存在し、DOCUMENT_ROOT 内に正規化され、それ自体が PHP_DENY_PATHS にマッチしてはなりません(ループ防止。そうでなければ起動が中断されます)。スクリプトは、元のリクエストを識別する2つの追加の $_SERVER キーとともに実行されます。

$_SERVER キー
OXPHP_DENIED_PATH 元のサニタイズ済みURI。先頭に / を付ける(PATH_INFO と同じ形式)
OXPHP_DENIED_PATTERN マッチしたglobパターン

OXPHP_DENIED_PATTERN は先頭の / なしで格納されます(glob正規化済み)が、OXPHP_DENIED_PATH はリクエストURIの / を保持します。パスをパターンと比較する場合は、両者を同じ形式にするために、まず ltrim($_SERVER['OXPHP_DENIED_PATH'], '/') としてください。

ハニーポットの例です。

/_security/denied.php
<?php // /_security/denied.php — runs in place of any matched .php request. error_log(sprintf( "PHP execution denied: path=%s pattern=%s ip=%s ua=%s", $_SERVER['OXPHP_DENIED_PATH'] ?? '', $_SERVER['OXPHP_DENIED_PATTERN'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '', $_SERVER['HTTP_USER_AGENT'] ?? '-', )); http_response_code(404); echo "Not Found";

これにより、1つの静的なステータスに限定される代わりに、リクエストごとにレスポンスを決定できます(攻撃者には404を返し、認証済みの管理者には403を返し、探索スキャナーをシンクホールにリダイレクトするなど)。

存在オラクルなし

StatusScript のどちらのフォールバックも、ファイルシステムに触れることなく返されます。/uploads/never-uploaded.php へのリクエストと /uploads/actually-on-disk.php へのリクエストは、同一のレスポンスを生成します。タイミングの差もボディの差もありません。アップロードされたシェルをスキャンする攻撃者は、拒否リストを使ってどのファイル名が存在するかを列挙することはできません。

解決済みパスのスクリーニングが唯一の例外です。これは必然的にルート解決の後で実行されるため、その拒否は存在に依存します。/uploads/uploads/index.php が実際にディスク上に存在する場合にのみ拒否されます。同様に、/uploads/*.php のような単一スター(single-star)のパターンでは、PATH_INFO リクエスト /uploads/shell.php/xuploads/shell.php が存在する場合にのみ拒否されます(完全なURIはパターンにマッチせず、解決済みスクリプトがマッチします)。攻撃者が探る対象である直接URIのスクリーニングは、オラクルフリーのままです。

可観測性

メトリクス 説明
oxphp_php_deny_total 拒否されたリクエストごとにインクリメントされるカウンター

各拒否は tracing::info ログも生成します。

text
PHP execution denied by PHP_DENY_PATHS path=uploads/shell.php pattern=uploads/**

アクセスログは結果のステータス(PHP_DENY_FALLBACK の値、またはフォールバックスクリプトの http_response_code())を記録します。拒否されたリクエストは、アクセスログのレベルでは通常のリクエストと区別されません。スパイクの原因を特定するには、メトリクスまたは構造化ログと相互参照してください。

パフォーマンス

マッチングは globset::GlobSet のルックアップで、通常はURIバイト列に対する単一のSIMDパスです。ヒットするとルートキャッシュもバイパスされます(拒否されるURIは、実質的に無制限のカーディナリティを持つ攻撃者制御下のスプレー攻撃に由来します。それらをキャッシュすると、攻撃者がLRUから正当なエントリを追い出せてしまいます)。ヒットパスとミスパスは、ウォームアップ後はどちらもアロケーションフリーです。

制限事項

この機能が行わないこと。

  • リテラルなファイルパターンに対するPATH_INFOバイパス。 /admin/legacy.php というパターンは /admin/legacy.php/extra にはマッチしません。両方をカバーするには /admin/legacy.php{,/**} を使うか、ディレクトリパターンを使ってください。
  • 大文字・小文字を区別するマッチング(上記の大文字・小文字の区別を参照)。
  • 正規表現なし。 パターンはglobのみで、アンカーされ、*/**/?/[abc]/{a,b} の演算子を持ちます。(a|b) の代わりにカンマ区切りの複数パターンを使ってください。
  • include / require / eval には影響なし。 拒否リストが管理するのは直接URIの実行のみです。include $_GET['page'] を行う脆弱なスクリプトは、依然としてサーバーが読み取り可能な場所ならどこからでもPHPを読み込めます。

非推奨のエイリアス

レガシーな PHP_DENY_DIRS 変数は非推奨のエイリアスとして受け入れられ、起動時に警告を出します。

text
WARN PHP_DENY_DIRS is deprecated, use PHP_DENY_PATHS instead — the alias will be removed in a future release

両方が設定されている場合は PHP_DENY_PATHS が優先され、PHP_DENY_DIRS は無視されたものとして報告されます。値はマージされません。

関連項目