CVE-2026-42945 — NGINX rewrite heap buffer overflow (CWE-122)¶
Critical severity
CVSS v4.0: 9.2 CRITICAL · CVSS v3.1: 8.1 HIGH · Unauthenticated remote attacker, single HTTP request.
| Field | Detail |
|---|---|
| Project | NGINX |
| Affected component | HTTP rewrite module (src/http/ngx_http_script.c); Open Source and NGINX Plus |
| Severity | HIGH |
| CVSS v4.0 | 9.2 CRITICAL (AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) |
| CVSS v3.1 | 8.1 HIGH (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-122 (Heap-based Buffer Overflow) |
| Affected versions | Open Source 0.6.27 – 1.30.0; NGINX Plus R32 – R36 |
| Fixed versions | Open Source 1.31.0 or 1.30.1; NGINX Plus R36 P4, R35 P2, R32 P6 |
| Fixed date | 2026-05-13 |
1. Vulnerability overview¶
NGINX's HTTP rewrite module contains a heap buffer overflow that an unauthenticated remote attacker can trigger with a single crafted HTTP request. On systems with ASLR disabled, the overflow can lead to full remote code execution. On all other systems it reliably crashes the NGINX worker process. The bug has been present since NGINX 0.6.27 (2008) and was fixed in NGINX 1.31.0 / 1.30.1, released on 2026-05-13.
Root cause¶
File: src/http/ngx_http_script.c
Function: ngx_http_script_regex_end_code()
Patch commit: 2046b45aa0c6e712c216b9075886f3f26e9b4ca9
NGINX's rewrite engine builds the replacement URI in two passes. First, a length pass computes how many bytes are needed to hold the result; second, a copy pass writes the final bytes into a heap-allocated buffer of exactly that size.
The length pass uses a temporary sub-engine (le) whose is_args flag starts at zero. Because le.is_args = 0, the function ngx_http_script_copy_capture_len_code() counts the raw (un-encoded) length of each PCRE capture group, one byte per source byte.
The copy pass runs on the main engine (e). If the rewrite replacement string contains a ? character, ngx_http_script_start_args_code() sets e->is_args = 1 to signal that what follows is a query string. This flag is never cleared before the copy pass processes subsequent directives. When a set, rewrite, or if directive then copies a capture variable (e.g. $1) into the buffer, ngx_http_script_copy_capture_code() sees is_args = 1 and calls ngx_escape_uri(..., NGX_ESCAPE_ARGS), which percent-encodes every URI-unsafe byte as %XX and triples its on-wire size. The buffer was only sized for raw bytes, so the copy overflows it by up to 2 × N bytes, where N is the number of escapable bytes in the capture.
A minimal vulnerable configuration looks like this:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true; # sets e->is_args = 1
set $original_endpoint $1; # copy pass overflows here
}
The fix is a single line added at the top of ngx_http_script_regex_end_code():
--- a/src/http/ngx_http_script.c
+++ b/src/http/ngx_http_script.c
@@ -1202,6 +1202,7 @@ ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)
r = e->request;
+ e->is_args = 0;
e->quote = 0;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
Resetting e->is_args to zero at the end of each rewrite replacement makes the length pass and copy pass of any subsequent directive start from the same baseline, so there is no unexpected URI-encoding expansion and no size mismatch.
2. Vulnerable environment¶
The reproduction environment is a Docker Compose stack with two NGINX containers, both built from the same Dockerfile via a build argument. Both are compiled from upstream NGINX source with AddressSanitizer enabled (-fsanitize=address -fno-omit-frame-pointer -g -O1) so that any heap overflow produces a symbolized backtrace identifying the vulnerable function.
| Container | Role | Port |
|---|---|---|
cve-2026-42945-nginx |
Vulnerable NGINX 1.30.0 (target) | 127.0.0.1:19321 → 80 |
cve-2026-42945-nginx-patched |
Patched NGINX 1.30.1 (discriminator) | 127.0.0.1:19322 → 80 |
Both containers run the NGINX master as root (PID 1); the worker process drops to the unprivileged nobody:nogroup user. The vulnerable container is pinned to NGINX 1.30.0 commit 6e14e954aaacce9a433d9b07b4653809c7594ab8; the patched container uses 1.30.1 commit 9a503b1317c283e1fd0f27008428ea441c1ac9ee.
The environment files are available as env.zip. Individual files: env/Dockerfile, env/docker-compose.yml, env/config/nginx.conf, env/config/entrypoint.sh.
To stand up the environment:
To confirm you are on the vulnerable version and that the two-directive configuration is in place:
# Both health endpoints should return "alive"
curl -fsS http://127.0.0.1:19321/healthz
curl -fsS http://127.0.0.1:19322/healthz
# Confirm container and process state
docker compose -f env/docker-compose.yml ps
docker exec cve-2026-42945-nginx ps -eo pid,user,args | grep 'worker process'
# Confirm no pre-existing ASan reports (clean baseline)
docker exec cve-2026-42945-nginx sh -c 'ls /tmp/asan.log* 2>/dev/null || echo "clean: no asan reports"'
docker exec cve-2026-42945-nginx grep -c "exited on signal" /usr/local/nginx/logs/error.log
The last two commands should produce clean: no asan reports and 0 respectively on a clean baseline.
The nginx.conf contains the trigger location ~ ^/api/(.*)$ with the rewrite ... /internal?migrated=true; directive followed by set $original_endpoint $1;. ASAN is configured with ASAN_OPTIONS log_path=/tmp/asan.log so that the worker (running as nobody) can write its crash report to the world-writable /tmp directory. On every container start, entrypoint.sh truncates the error logs and removes any prior /tmp/asan.log.* files, so each restart begins from a clean baseline.
3. How to exploit¶
The exploit sends a single unauthenticated HTTP GET request to the ~ ^/api/(.*)$ location, with a capture payload made up entirely of + characters. Each + is URI-unsafe and percent-encodes to %2B (three bytes). Sending 8000 + characters makes the copy pass write 24000 bytes (8000 × 3) into an 8000-byte heap buffer, overflowing it by 16000 bytes and crashing the worker.
The exploit files are available as exploit.zip. Individual file: exploit/run.sh.
Steps¶
Step 1. Confirm the environment is running and the baseline worker is alive.
Expected output: alive. Note the current worker PID:
Step 2. Run the exploit script.
The script issues GET /api/<8000 '+'> with --path-as-is so the + characters reach the server unmodified. Expected client-side output:
[*] Target: http://127.0.0.1:19321/api/<8000 '+'>
[*] Sending single crafted request...
curl: (52) Empty reply from server
[*] http_code=000 curl_exit_pending
[*] curl exit code: 52
The connection reset (curl: (52), http_code=000) is consistent with the worker dying mid-request. This client-side observation is not treated as proof. The proof is read from the server side independently.
Step 3. Verify the crash from the server side.
Read the NGINX master log to confirm the worker was killed by signal 6 (SIGABRT, triggered by AddressSanitizer):
Expected output:
Confirm a new worker has replaced the crashed one (PID change from 17 to 61 in the verified run):
Read the AddressSanitizer heap-buffer-overflow report written by the dying worker:
The report from the verified run shows:
==17==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 1
#0 ngx_escape_uri src/core/ngx_string.c:1689
#1 ngx_http_script_copy_capture_code src/http/ngx_http_script.c:1399
#2 ngx_http_rewrite_handler src/http/modules/ngx_http_rewrite_module.c:180
...
0xffff9d607040 is located 0 bytes to the right of 8000-byte region
allocated by ... ngx_http_script_complex_value_code src/http/ngx_http_script.c:1778
The backtrace frames #1 ngx_http_script_copy_capture_code → #0 ngx_escape_uri are precisely the code path that the e->is_args = 0 patch removes. The heap region reported as overflowed is 8000 bytes, the exact size of the raw + capture, which confirms the size mismatch between the length pass and the copy pass.
This evidence comes from independent AddressSanitizer instrumentation inside the NGINX worker process itself, separate from the exploit script. The exploit script only sends one HTTP request via curl. It does not read /tmp/asan.log.*, does not run docker exec, and cannot fabricate either the ASan report or the master-log signal-6 line.
Step 4. Run the same exploit against the patched discriminator to confirm attribution.
Expected: HTTP 200, curl exit code 0, worker PID unchanged, no ASan reports, zero signal-exit lines in the error log. The verified run confirmed all of these. The same request that kills 1.30.0 is harmless on 1.30.1, which attributes the crash to this vulnerability rather than to generic large-request behavior.
Environment teardown¶
When done, stop and remove the environment:
4. Security advice¶
Remediation¶
Upgrade immediately. The one-line fix (e->is_args = 0; in ngx_http_script_regex_end_code()) is present in:
- NGINX Open Source 1.31.0 (mainline) or 1.30.1 (stable), released 2026-05-13
- NGINX Plus R36 P4, R35 P2, R32 P6
Any NGINX Open Source installation from 0.6.27 through 1.30.0, or NGINX Plus R32 through R36 without a patch release, is vulnerable if its configuration contains a rewrite directive with ? in the replacement string in the same location block as a subsequent set, rewrite, or if directive using an unnamed capture variable.
Mitigations and workarounds¶
If an immediate upgrade is not possible:
- Audit
rewritedirectives. Locate everylocationblock that contains arewritedirective whose replacement includes a?character, followed by anyset/rewrite/ifdirective referencing an unnamed capture ($1,$2, etc.). Refactor such blocks to eliminate the named-capture +?combination, or restructure the rewrite logic to avoid the two-pass size mismatch. - Named captures are not affected. The overflow occurs only through unnamed PCRE captures (
$1,$2). Converting unnamed captures to named captures (e.g.(?P<name>...)and$name) avoids the vulnerable code path in the copy pass. - Network-level filtering. Blocking or sanitizing URI payloads at a WAF or upstream proxy can reduce exploitability but is not a reliable mitigation for a memory-corruption bug; upgrade remains the correct fix.
References¶
- NVD CVE-2026-42945: CVE record, CWE, CVSS scores
- GHSA-gcgv-v5gf-c543: GitHub Security Advisory with affected and fixed versions
- DepthFirst blog: NGINX Rift: affected versions, root cause overview, mitigation guidance
- DepthFirst technical write-up: detailed two-pass engine analysis and full RCE exploit mechanics
- DepthFirstDisclosures/Nginx-Rift (PoC repo): full RCE PoC code and vulnerable Docker environment
- nginx/nginx patch commit 2046b45: the one-line fix,
e->is_args = 0inngx_http_script_regex_end_code() - NGINX changelog (nginx.org): confirms the fix in 1.31.0 (2026-05-13)
- F5 advisory K000161019: NGINX Plus affected and fixed version ranges