0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nginx で multipart リクエストを PHP の手前で弾く設定を読み解く:map 変数はリクエスト内でキャッシュされる(php2shell-hardening の error_page ループと volatile)

0
Posted at

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 が公開された。
認証なしのリモートコード実行につながりうる重大な脆弱性として、公式は更新を強く勧めている。

出典:

このパッチが出るまでの時間稼ぎとして公開されたのが、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 として解析できるから

([;,\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 $uri may 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.

出典:nginx core module ドキュメント

map 3 が無いと、multipart の POST は次の流れで 500 になる。

  1. map 1 が 1、map 2 が 0 なので $pp_block_mp が 1 になり、return 403 する
  2. error_page が /error/403.html へ内部リダイレクトする
    • メソッドは GET に変わる
    • $uri は /error/403.html になる
    • $request_uri は変わらない
  3. 内部リダイレクトなので、server レベルの if がもう一度評価される
  4. $pp_block_mp がまた 1 になり、再び return 403 する
  5. 2〜4 を繰り返す。内部リダイレクトは 1 リクエストにつき 10 回までなので、上限に達すると 500 を返す
  6. エラーログに 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 $uri may change during request processing, e.g. when doing internal redirects, or when using index files.

($uri)

full original request URI (with arguments)

($request_uri)

出典:nginx core module ドキュメント

手順 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.

出典:nginx core module ドキュメント

実際に確かめた。
リポジトリの 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.

volatile indicates that the variable is not cacheable (1.11.7)

出典:nginx map ドキュメント

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 を攻撃の必須条件としていない
  • 許可したルートは守られない(章 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 は、公式もワークアラウンドに挙げている

❌ 「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 を書くなら、自分自身を踏み直すことを前提に設計する

この設定は更新までの時間稼ぎだが、読み解いて得た勘所は更新後も残る。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?