1. はじめに
php2shell と呼ばれる PHP の脆弱性(CVE-2026-109618 / CVE-2026-109619)に対して、
2026-10-21 に PHP 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 が公開された。
認証なしのリモートコード実行につながりうる重大な脆弱性として、公式は更新を強く勧めている。
出典:
- PHP.net アーカイブ(2026 年):https://www.php.net/archive/2026.php
このパッチが出るまでの時間稼ぎとして公開されたのが、GitHub の
php2shell-hardening リポジトリである。
この記事は、その nginx/ ディレクトリにある 2 ファイルを 1 行ずつ読み解く。
読み解く対象は、2026-10-22 時点のコミット 89f92b0 である。
結論を先に書く。
判定ロジックは全部 map に寄せて if は return 403 だけにする。ただし map で作った変数は一度評価されるとそのリクエストの中でキャッシュされる。error_page の内部リダイレクトで同じ if がもう一度評価されるとき、volatile の無い map はリダイレクト前の値を返し続けるので、403 が 500 に化ける。
この記事で分かること:
-
mapで判定を組み、ifはreturnだけにする形 -
$request_uriと$uriの使い分け -
error_pageの内部リダイレクトと、map 変数のキャッシュ・volatile
👉 本命は PHP の更新である。この設定はパッチ前の時間稼ぎとして書かれたもので、読み解くと nginx の勘所が詰まっている。
2. 全体像
nginx 版は 2 ファイルで構成される。
| ファイル | 置く場所 | 役割 |
|---|---|---|
nginx/php2shell-maps.conf |
http {} |
map を 4 つ定義する |
nginx/php2shell-server-snippet.conf |
server {} |
if を 1 行置く |
map は次の 4 つである。この記事では「map 1〜4」と変数名を併記して呼ぶ。
| 呼び名 | 変数 |
|---|---|
| map 1 | $pp_mp |
| map 2 | $pp_upload_route |
| map 3 | $pp_errpage |
| map 4 | $pp_block_mp |
処理の流れは次のとおり。
- map 1:multipart のリクエストか
- map 2:許可したルートか
- map 3:エラーページへの内部リダイレクトか
- map 4:上の 3 つを 3 桁にまとめて最終判定する
-
server {}のif ($pp_block_mp) { return 403; }が、判定が真のときだけ 403 を返す
リポジトリには nginx 以外のディレクトリもある。
-
apache:.htaccessと ModSecurity 向け -
caddy:Caddy 向け -
cloudflare:Cloudflare 向け -
frankenphp:FrankenPHP 向け -
lighttpd:lighttpd 向け -
php-fpm:php-fpm 向け(99-php2shell.ini) -
test:動作確認用の設定
👉 以降は nginx 版のみを扱う。
3. map 1:multipart かどうか
最初の map は、リクエストが multipart かどうかを 0 / 1 で返す。
# multipart detector. $content_type is nginx's Content-Type header var (the same value PHP
# sees via fastcgi_params CONTENT_TYPE). PHP lowercases the header and truncates it at the
# first ';', ',' or space, then looks for "boundary" anywhere, so
# "multipart/form-data,boundary=x" is multipart to PHP. The regex covers that grammar.
# PUT/PATCH are included because PHP 8.4 request_parse_body() parses multipart on them too.
map "$request_method:$content_type" $pp_mp {
default 0;
"~*^(POST|PUT|PATCH):\s*multipart/form-data([;,\s]|$)" 1;
}
読み解く。
-
$request_method:$content_typeを連結して、正規表現 1 本で判定している -
~*は大文字小文字を無視する正規表現(出典:nginx map ドキュメント)-
MULTIPART/FORM-DATAのような大文字でも 1 になる
-
- PUT / PATCH を含めているのは、PHP 8.4 の
request_parse_body()が PUT / PATCH の body も multipart として解析できるから- 出典:php-src
ext/standard/http.c - 通常の POST データ読み込みは、メソッドが
POSTのときだけ行われる - 出典:php-src
main/SAPI.c
- 出典:php-src
([;,\s]|$) に , が入っている理由
PHP は Content-Type を先頭から走査する。
; , 空白のどれかで切って終端し、それ以外の文字は小文字にする。
for (p = content_type; p < content_type + content_type_length; p++) {
switch (*p) {
case ';':
case ',':
case ' ':
content_type_length = p-content_type;
oldchar = *p;
*p = 0;
break;
default:
*p = tolower((unsigned char)*p);
break;
}
}
出典:php-src main/SAPI.c(L184 以降を抜粋。直前のコメント行は省略)
- 切られたあとの文字列は
multipart/form-dataになる - その後、
boundaryという文字列をstrstrで探す - つまり
multipart/form-data,boundary=xは、PHP から見ると multipart である
👉 ; の後だけを見るルールだと、multipart/form-data,boundary=x は素通りする。
区切りに , を入れた正規表現は、この PHP の解釈に合わせている。
4. map 2:アップロードを許可するルート
2 つめの map は、アップロードを許可するルートかどうかを返す。
# Routes that must keep accepting uploads. ADAPT this list to your app, from your logs.
# Matches $request_uri (the original URI, not $uri which becomes /index.php after try_files),
# anchored to a segment boundary so /upload does not also open /uploads-report or /uploader.
map $request_uri $pp_upload_route {
default 0;
"~*^/upload(/|\?|$)" 1;
"~*^/api/upload(/|\?|$)" 1;
"~*^/wp-admin/async-upload\.php(/|\?|$)" 1;
}
読み解く。
- 判定に
$request_uriを使っている- 原文のコメントのとおり、
$uriはtry_filesのあとに/index.phpに書き換わる -
$request_uri:「full original request URI (with arguments)」 -
$uri:「current URI in request, normalized」「The value of$urimay change during request processing, e.g. when doing internal redirects, or when using index files.」 - 出典:nginx core module ドキュメント
- 原文のコメントのとおり、
-
(/|\?|$)でセグメントの境界に固定している-
/upload、/upload/img、/upload?x=1は許可される -
/uploads-reportや/uploaderは許可されない
-
- この 3 行は例である。自分のアプリのアップロード先に書き換える前提で、原文のコメントも「from your logs」と書いている
👉 $request_uri は変わらない値、$uri は変わる値。用途で使い分ける。
❌ 「許可したルートは守られる」ではない。
multipart のパーサは認証やアプリのコードより前に動く。
許可したルートは、この設定では何も守られず、脆弱なまま残る(出典:リポジトリ README の Limits)。
5. map 3 と volatile:error_page のループ
3 つめの map は、エラーページへの内部リダイレクトを判定から外すためのものである。
この章が、この設定でいちばん nginx らしい落とし穴を含んでいる。
5-1. 何が起きるか
map 3 を、直前のコメントごと原文で引用する。
# Error-page guard (CRITICAL, do not remove). With `error_page 403 /error/403.html` +
# `recursive_error_pages on` (the ISPConfig default), returning 403 triggers an internal
# redirect to the error page that re-evaluates the server-level `if` with the SAME method and
# Content-Type -> "rewrite or internal redirection cycle" -> HTTP 500 (PHP is still not
# reached, but the response is wrong and the log fills with errors). This matches $uri, which
# DOES change to the error page on the internal redirect (unlike $request_uri), and excludes
# it. `volatile` is mandatory: without it nginx reuses the value computed before the redirect
# and the cycle stays. Verified on nginx 1.24.
map $uri $pp_errpage {
volatile;
default 0;
"~^/error/[0-9]{3}\.html$" 1;
}
前提は次の 2 行である。原文のコメントによれば、ISPConfig ではこれがデフォルトになっている。
error_page 403 /error/403.html;recursive_error_pages on;
recursive_error_pages は、error_page による内部リダイレクトを何度も行うかどうかの設定である。
Enables or disables doing several redirects using the error_page directive. The number of such redirects is limited.
map 3 が無いと、multipart の POST は次の流れで 500 になる。
- map 1 が 1、map 2 が 0 なので
$pp_block_mpが 1 になり、return 403する -
error_pageが/error/403.htmlへ内部リダイレクトする- メソッドは GET に変わる
-
$uriは/error/403.htmlになる -
$request_uriは変わらない
- 内部リダイレクトなので、server レベルの
ifがもう一度評価される -
$pp_block_mpがまた 1 になり、再びreturn 403する - 2〜4 を繰り返す。内部リダイレクトは 1 リクエストにつき 10 回までなので、上限に達すると 500 を返す
- エラーログに
rewrite or internal redirection cycleが出る
手順 2 の根拠は、公式ドキュメントの次の 3 か所である。
This causes an internal redirect to the specified uri with the client request method changed to “GET” (for all methods other than “GET” and “HEAD”).
(error_page)
current URI in request, normalized
The value of
$urimay change during request processing, e.g. when doing internal redirects, or when using index files.
($uri)
full original request URI (with arguments)
($request_uri)
手順 5・6 の根拠は internal の説明にある。
There is a limit of 10 internal redirects per request ... If this limit is reached, the error 500 (Internal Server Error) is returned. In such cases, the “rewrite or internal redirection cycle” message can be seen in the error log.
実際に確かめた。
リポジトリの test/nginx-test-broken.conf(map 3 無し、volatile 無し)を nginx:1.24-alpine に読ませて curl で叩いた。
結果は次のとおり。
GET / 200
POST urlencoded / 200
POST multipart / 500
POST multipart /upload 200
POST multipart /upload/img 200
POST multipart /uploads-report 500
POST multipart,boundary (comma) / 500
POST MULTIPART/FORM-DATA uppercase / 500
PUT multipart / 500
PATCH multipart / 500
POST json / 200
--- body of POST multipart /:
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.24.0</center>
</body>
</html>
--- nginx error log (last 5):
2026/10/22 05:02:40 [error] 30#30: *8 rewrite or internal redirection cycle while internally redirecting to "/error/403.html", client: 172.17.0.1, server: , request: "POST / HTTP/1.1", host: "127.0.0.1:18089"
2026/10/22 05:02:40 [error] 30#30: *9 rewrite or internal redirection cycle while internally redirecting to "/error/403.html", client: 172.17.0.1, server: , request: "POST / HTTP/1.1", host: "127.0.0.1:18089"
2026/10/22 05:02:40 [error] 30#30: *10 rewrite or internal redirection cycle while internally redirecting to "/error/403.html", client: 172.17.0.1, server: , request: "PUT / HTTP/1.1", host: "127.0.0.1:18089"
2026/10/22 05:02:40 [error] 30#30: *11 rewrite or internal redirection cycle while internally redirecting to "/error/403.html", client: 172.17.0.1, server: , request: "PATCH / HTTP/1.1", host: "127.0.0.1:18089"
2026/10/22 05:02:40 [error] 30#30: *13 rewrite or internal redirection cycle while internally redirecting to "/error/403.html", client: 172.17.0.1, server: , request: "POST / HTTP/1.1", host: "127.0.0.1:18089"
- 403 になるはずの multipart がすべて 500 になっている
- エラーログには、
/error/403.htmlへの内部リダイレクトがループした記録が残っている - 後ろの
location /(本番なら PHP に渡す場所)には届いていない。ただし応答は 403 ではなく、ログもエラーで埋まる
5-2. なぜまた 1 になるか:map 変数はキャッシュされる
ここで疑問が残る。
手順 2 でメソッドは GET に変わる。map 1 の正規表現は POST|PUT|PATCH しか拾わない。
それなら手順 4 で map 1 は 0 になり、ループは止まるはずである。
止まらない理由は、map 変数のキャッシュにある。公式ドキュメントの関係する記述は次の 2 つである。
Since variables are evaluated only when they are used, the mere declaration even of a large number of “map” variables does not add any extra costs to request processing.
volatileindicates that the variable is not cacheable (1.11.7)
volatile は「キャッシュしない」という意味である。
裏返すと、volatile の無い map 変数はキャッシュされうる。
これがリクエストの中でどう効くのかを、エラーページの中で変数を出力して確かめた。
リポジトリの test/nginx-test.conf から変えたのは、$pp_mp_v の map の追加と、エラーページの location の書き換えの 2 か所である。
ほかに、map 2 を /upload の 1 行に縮め、location /upload を消し(location / も同じ REACHED を返す)、map 3・4 を 1 行に書き直した(中身は同じ)。
どれも今回の判定には関わらない。
map "$request_method:$content_type" $pp_mp_v {
volatile;
default 0;
"~*^(POST|PUT|PATCH):\s*multipart/form-data([;,\s]|$)" 1;
}
location = /error/403.html { internal; return 200 "method=$request_method uri=$uri request_uri=$request_uri pp_mp=$pp_mp pp_mp_volatile=$pp_mp_v\n"; }
-
$pp_mp_vは map 1 と同じ定義にvolatile;を足しただけの比較用の変数 - エラーページのボディで、メソッド・
$uri・$request_uri・$pp_mp・$pp_mp_vを出力する
POST multipart / と PUT multipart / を送ると、どちらも次の同じボディが返った。
method=GET uri=/error/403.html request_uri=/ pp_mp=1 pp_mp_volatile=0
3 点で読む。
- メソッドは
GETに変わっている(error_pageの仕様どおり) -
$uriは/error/403.htmlに変わり、$request_uriは/のまま -
volatile無しの$pp_mpは1のまま。volatile付きの$pp_mp_vは0
同じ定義でも、volatile の有無で値が分かれた。
実測では、volatile の無い map 変数は、リダイレクト前に計算した値をそのまま返している。
つまりループが止まらないのは、volatile の無い map 変数(map 1 と最終判定の $pp_block_mp)がリダイレクト前の値を返し続けるからである。
ここでは map 1 の値を直接見た。最終判定の map 側の切り分けは 5-4 で示す。
👉 先に書いておくと、リポジトリの設定そのものは正しく動く(章 7 の実測で示す)。
そのうえで、原文のコメントと実測の値を並べておく。
| 内容 | |
|---|---|
| 原文のコメント | 「re-evaluates the server-level if with the SAME method and Content-Type」 |
| 実測(Content-Type) | multipart のまま(5-4 の実測で確認) |
| 実測(メソッド) | method=GET |
5-3. リポジトリの対策:$uri を見るガード
map 3 は「$uri がエラーページなら 1」を返す。
- 内部リダイレクトで変わるのは
$uriである - 変わる側を見れば、「いまエラーページを処理している」ことが分かる
- 変わらない
$request_uriを見る map 2 とは、ちょうど逆の使い方になる
map 4 の判定は 3 桁の "100" だけが 1 である。エラーページの中では次のようになる。
| 桁 | 変数 | エラーページの中での値 |
|---|---|---|
| 1 桁目 | $pp_mp |
1(キャッシュされたまま) |
| 2 桁目 | $pp_upload_route |
0 |
| 3 桁目 | $pp_errpage |
1 |
- キャッシュされた
$pp_mp = 1はそのまま残す -
$pp_errpage = 1で 3 桁を101にし、"100"に当たらないようにして打ち消す
これが動く条件は、$pp_errpage と $pp_block_mp の両方に volatile が付いていることである。
修正版から volatile; の 2 行だけを消すと、5-1 と同じく multipart はすべて 500 になり、エラーログに rewrite or internal redirection cycle が出た。
👉 volatile を付けた正しい形で 403 とカスタムエラーページが返ることは、章 7 の実測で示す。
5-4. 別解と切り分け
別解:map 1 に volatile を付ける
$pp_errpage を使わず、map 1 に volatile を付ける形でもループは止まる。
リポジトリの test/nginx-test.conf からの変更点は次のとおり。
map "$request_method:$content_type" $pp_mp {
volatile;
default 0;
"~*^(POST|PUT|PATCH):\s*multipart/form-data([;,\s]|$)" 1;
}
map $request_uri $pp_upload_route { default 0; "~*^/upload(/|\?|$)" 1; }
map "$pp_mp$pp_upload_route" $pp_block_mp { volatile; default 0; "10" 1; }
location = /error/403.html { internal; return 200 "BLOCKED-403 method=$request_method ct=$content_type\n"; }
- map 1 に
volatile;を足した - map 3(
$pp_errpage)は無い - map 4 は 2 桁の
"10"で判定し、volatile;を付けた - 確認のため、エラーページの
locationを書き換え、ボディでメソッドと$content_typeを出力する - map 2 は
/uploadの 1 行に縮め、location /uploadは消した(location /が同じREACHEDを返す)
結果は次のとおり。
POST multipart / -> BLOCKED-403 method=GET ct=multipart/form-data; boundary=------------------------PgQ6ab7UPXB48Jvx5P3xlv
[403]
PUT multipart / -> BLOCKED-403 method=GET ct=multipart/form-data; boundary=------------------------ycina5U9awwqSlIT3nLZT1
[403]
PATCH multipart / -> BLOCKED-403 method=GET ct=multipart/form-data; boundary=------------------------M7xy50RTN1RkoW3ofEuOj1
[403]
POST multipart /upload -> REACHED
[200]
POST urlencoded / -> REACHED
[200]
- エラーログは空だった
-
$content_typeは multipart のままである - メソッドが GET なので、
volatile付きの map 1 は 0 になり、ループしない
切り分け:どの volatile が効いているか
volatile を 1 つずつ外して、POST multipart / を送った。
| 構成 | 結果 |
|---|---|
修正版から $pp_errpage の volatile だけ削除 |
500、rewrite or internal redirection cycle
|
修正版から $pp_block_mp の volatile だけ削除 |
500、同上 |
上の別解から $pp_block_mp の volatile だけ削除 |
500、同上 |
👉 「変わる値を供給する map」($pp_errpage または $pp_mp)と「最終判定の map」($pp_block_mp)の両方に volatile が要る。
片方だけでは、供給側か最終判定側のどちらかがリダイレクト前の値を返し、最終判定がまた 1 になる。
❌ 「volatile は念のため付けておくもの」ではない。
この構成では、無いと 500 になる。
👉 recursive_error_pages off(デフォルト)なら 500 にはならない。
ただしカスタムエラーページも出ず、nginx のデフォルトの 403 ページが返る。
壊れた版を off にして試すと、ボディの title 行は次のとおりだった。
<title>403 Forbidden</title>
on のままカスタムページを出しつつ 500 を避けるには、リポジトリの形か、上の別解が要る。
6. map 4 と if:最終判定
4 つめの map は、ここまでの 3 つの値を連結して、最終的に block するかどうかを決める。
# Final decision: block only when multipart=1, upload-route=0, error-page=0 -> "100".
map "$pp_mp$pp_upload_route$pp_errpage" $pp_block_mp {
volatile;
default 0;
"100" 1;
}
読み解く。
- 3 つの変数を
"$pp_mp$pp_upload_route$pp_errpage"の順に連結し、3 桁の文字列にしている -
"100"のときだけ 1 を返し、それ以外はdefault 0になる -
volatile;が付いている(章 5 のとおり、最終判定の map にも必要)
3 桁は、左から $pp_mp / $pp_upload_route / $pp_errpage である。
代表的な組み合わせを決定表にする。
$pp_mp |
$pp_upload_route |
$pp_errpage |
連結 | $pp_block_mp |
|---|---|---|---|---|
| 1 | 0 | 0 | 100 |
1 |
| 0 | 0 | 0 | 000 |
0 |
| 0 | 1 | 0 | 010 |
0 |
| 1 | 0 | 1 | 101 |
0 |
| 1 | 1 | 0 | 110 |
0 |
-
100:multipart で、許可ルートではなく、エラーページでもない。これだけ block する -
000:通常のリクエスト。通す -
010:multipart ではない許可ルート。通す -
101:エラーページの中。章 5 のとおり、キャッシュされた$pp_mp = 1を$pp_errpage = 1が打ち消す -
110:許可ルートへの multipart。通す
server 側の 1 行
server {} に置く snippet を、コメントも含めて全文引用する。
# Include INSIDE the server{} block (before the locations that pass to PHP).
# Requires php2shell-maps.conf loaded in http{}.
if ($pp_block_mp) { return 403; }
# Note: if you do NOT use error_page/recursive_error_pages you may use `return 444;` to drop
# the connection with no error-page redirect at all. If you DO use error_page (e.g. ISPConfig),
# keep 403: the $pp_errpage guard in php2shell-maps.conf prevents the redirect cycle that would
# otherwise turn the 403 into a 500.
読み解く。
-
if ($pp_block_mp) { return 403; }が、判定が真のときだけ 403 を返す - 判定の中身は全部 map にある。
ifは変数を 1 つ見てreturnするだけ
if は危険と言われるのに、なぜここでは使えるか
nginx の旧 wiki「If is Evil」は、if の危険性と安全な使い方を書いている。
アーカイブされた公式 wiki リポジトリの原文から、要点の 2 文を引用する。
The only 100% safe things which may be done inside if in a location context are: return ...; rewrite ... last; Anything else may possibly cause unpredictable behaviour, including potential SIGSEGV.
In some cases, it's also possible to move ifs to server level (where it's safe as only other rewrite module directives are allowed within it).
出典:https://github.com/nginxinc/nginx-wiki/blob/master/source/start/topics/depth/ifisevil.rst
- location コンテキストの
ifで 100% 安全なのは、return ...;とrewrite ... last;だけ -
ifを server レベルに置けば安全 - この構成は、判定を map に寄せて、
ifを server レベルのreturnだけにしている- 「安全な形」そのものである
👉 出典は旧 nginx wiki をアーカイブしたリポジトリである。
以前の nginx.com の URL は、いまは辿れない。
👉 snippet の注記のとおり、error_page を使わないなら return 444; で接続を切ってもよい。
error_page を使うなら 403 のままにする。
7. 実測
リポジトリの test/nginx-test.conf を、そのまま nginx に読ませて確かめた。
手順は 3 行である。
-
nginx:1.24-alpineに、test/nginx-test.confを/etc/nginx/nginx.confとしてマウントする - ホストから curl で 11 パターンを送る
- multipart は
-F a=1、urlencoded は-d a=1、それ以外の形は Content-Type を-Hで指定する。ステータスは-w '%{http_code}'で取る
使ったコマンドの要点は次のとおり($PWD はリポジトリのクローン直下)。
docker run -d --name pp-nginx-check -p 18089:80 \
-v "$PWD/test/nginx-test.conf:/etc/nginx/nginx.conf:ro" nginx:1.24-alpine
B=http://127.0.0.1:18089
curl -s -o /dev/null -w '%{http_code}\n' -X POST -F a=1 "$B/" # multipart
curl -s -o /dev/null -w '%{http_code}\n' -X POST -d a=1 "$B/" # urlencoded
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: multipart/form-data,boundary=x' --data-binary x "$B/" # カンマ区切り
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: MULTIPART/FORM-DATA; boundary=x' --data-binary x "$B/" # 大文字
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: application/json' -d '{}' "$B/" # JSON
test/nginx-test.conf の server {} 部分は次のとおりである。
server {
listen 80;
error_page 403 /error/403.html;
recursive_error_pages on;
if ($pp_block_mp) { return 403; }
location = /error/403.html { internal; return 200 "BLOCKED-403\n"; }
location /upload { return 200 "REACHED\n"; }
location / { return 200 "REACHED\n"; }
}
-
error_page 403 /error/403.html;とrecursive_error_pages on;がある - つまり章 5 のループが起きうる環境で、
volatileと$pp_errpageが効いているかを見る構成である -
/error/403.htmlはinternalで、BLOCKED-403を返す
結果は次のとおり。
GET / 200
POST urlencoded / 200
POST multipart / 403
POST multipart /upload 200
POST multipart /upload/img 200
POST multipart /uploads-report 403
POST multipart,boundary (comma) / 403
POST MULTIPART/FORM-DATA uppercase / 403
PUT multipart / 403
PATCH multipart / 403
POST json / 200
--- body of POST multipart /:
BLOCKED-403
--- nginx error log (last 5):
4 点で読む。
-
/uploadと/upload/imgは通り(200)、/uploads-reportは弾かれる(403) -
multipart/form-data,boundaryのカンマ区切りと、大文字のMULTIPART/FORM-DATAも弾かれる - POST だけでなく、PUT と PATCH の multipart も弾かれる
- ボディは
BLOCKED-403で、カスタムエラーページが出ている。エラーログは空(500 のループは起きていない)
👉 壊れた版との違いは、章 5 で示したとおりである。
8. 限界と注意
この設定は、PHP の更新の代わりにはならない。
注意点を 6 つ挙げる。
- 本命は PHP の更新である
- 修正版は 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35
- 公式のワークアラウンドの説明は「only an update is a complete solution」
- 出典:https://www.php.net/archive/2026.php
- 公式 advisory は、multipart を攻撃の必須条件としていない
- GHSA-w8hw-w9fx-j79w は「If the application does not use multipart form data, block such requests at the frontend before they reach PHP.」と書く一方で、「OOM can also be reached in application-specific ways」とも書いている
- GHSA-6wmf-4rv5-p3rp は「There is no complete generic configuration mitigation.」と書いている
- 出典:https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- 出典:https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
- 許可したルートは守られない(章 4 のとおり)
- multipart のパーサは、認証やアプリのコードより前に動く
- 正規のアップロードやフォームを壊す
- multipart を使う画面は、許可ルートに入れない限り 403 になる
- 本番に入れる前に、ステージングで試す
- PHP 8.1 以前には、PHP プロジェクトからの更新が無い
- ディストリビューションや LTS ベンダが、バックポートした修正を出していることがある
- 7.4 / 8.0 / 8.1 向けの最小ソースバックポートは、php.net の告知からリンクされている
- 出典:https://www.php.net/archive/2026.php
-
session.upload_progress.enabled=0は、公式もワークアラウンドに挙げている- ただし「This blocks the confirmed session paths but not other variants」とされている
- 出典:https://www.php.net/archive/2026.php
❌ 「multipart を弾けば安全」ではない。
👉 本当の対処は、PHP を修正版(8.5.12 / 8.4.27 / 8.3.36 / 8.2.35)に更新することである。
9. まとめ
この設定から持ち帰れる nginx の勘所は 4 点ある。
- 判定は
mapに寄せ、ifは server レベルでreturnだけにする -
$request_uriは変わらない方、$uriは内部リダイレクトで変わる方。用途で選ぶ - map 変数はリクエスト内でキャッシュされる。内部リダイレクトのあとに再計算させたい map には
volatileを付ける- 変わる値を供給する map と、最終判定の map の両方に要る
-
error_page+recursive_error_pages onで server レベルのreturn 403を書くなら、自分自身を踏み直すことを前提に設計する
この設定は更新までの時間稼ぎだが、読み解いて得た勘所は更新後も残る。