CVE-2026-42945 — NGINX Rift: Heap Buffer Overflow to Remote Code Execution¶
Published 2026-06-04
ASLR-disabled precondition
The remote code execution demonstrated on this page requires the target NGINX worker to run with address-space layout randomisation (ASLR) disabled. Under default Linux settings (ASLR enabled) the same bug causes a worker crash and restart (DoS). No public RCE technique for ASLR-enabled targets has been disclosed. See Security advice for mitigations regardless of ASLR state.
| Field | Detail |
|---|---|
| Vulnerability | CVE-2026-42945 / GHSA-gcgv-v5gf-c543 |
| Project | NGINX |
| Affected component | src/http/ngx_http_script.c (rewrite script engine); 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 |
| Impact | Unauthenticated RCE (ASLR off) · Worker crash / DoS (ASLR on) |
| Affected | NGINX OSS 0.6.27–1.30.0 · NGINX Plus R32–R36 |
| Fixed | NGINX OSS 1.30.1 (stable) / 1.31.0 (mainline) · NGINX Plus R36 P4, R35 P2, R32 P6 |
1. Vulnerability overview¶
NGINX is the world's most-deployed HTTP server and reverse proxy. CVE-2026-42945 ("NGINX Rift") is an 18-year-old heap-based buffer overflow in its rewrite script engine, a code path that has existed since NGINX 0.6.27 (2008) and is present in every NGINX release through 1.30.0.
The vulnerability is triggered when three configuration ingredients appear together
in a single location block:
- A
rewritedirective whose source pattern contains an unnamed PCRE capture group (e.g.,^/api/(.*)). - A literal
?in the rewrite replacement string (e.g.,/internal?migrated=true), which activates URI argument encoding. - A subsequent
set,if, orrewritedirective that references the captured group as$1,$2, etc.
An unauthenticated attacker sends a single crafted HTTP request to any such location. The overflow corrupts the adjacent heap, typically crashing the NGINX worker process. With ASLR disabled, the deterministic heap layout allows arbitrary command execution inside the worker, requiring no authentication and no prior knowledge of the target's configuration beyond the presence of the trigger pattern.
Root cause¶
File: src/http/ngx_http_script.c
Function: ngx_http_script_regex_end_code()
NGINX's rewrite script engine processes directives in two passes: a length-calculation
pass first measures the buffer space required, then an allocation-and-copy pass fills
the allocated buffer. The bug arises from a state flag, is_args, that is set to 1 when a ? is
seen in the replacement string (indicating that URI argument encoding should be
applied) but is never reset between the two passes.
The two passes use different engine instances. The length pass operates on a fresh
sub-engine le where le.is_args == 0, so it returns the raw, unescaped byte count
of the captured string. The copy pass operates on the main engine e where
e.is_args == 1, so it calls ngx_escape_uri(buf, capture, len, NGX_ESCAPE_ARGS),
which expands every URI-special byte (e.g., +, %, &) from 1 byte to 3 bytes
in percent-encoded form. The buffer is too small for the escaped output, and
attacker-controlled URI bytes overflow it.
The fix is a single line added by commit 2046b45aa0c6e712c216b9075886f3f26e9b4ca9:
// src/http/ngx_http_script.c — ngx_http_script_regex_end_code()
r = e->request;
+ e->is_args = 0; /* ← the one-line fix */
e->quote = 0;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
Resetting e->is_args to 0 immediately after the regex-end handler completes
makes both passes see the same flag value and agree on buffer sizing.
The minimum nginx.conf pattern that triggers the bug:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true; # sets is_args=1
set $original_endpoint $1; # copy pass expands $1
}
2. Vulnerable environment¶
The reproduction runs inside a single Docker container. The container builds NGINX
from the upstream git tree pinned at the pre-fix commit 98fc3bb78e8daef25c3d850c9cba8c2f787fb99e
(the authoritative PoC's own pin), leaving the source entirely unmodified. NGINX
self-reports version 1.31.0 at this commit because the dev version string was
bumped before the fix landed; the correct reference is the git commit hash, which
matches the PoC's pin character-for-character.
The container runs as linux/amd64 (via Docker Desktop QEMU emulation on ARM hosts)
because the PoC hardcodes x86-64 addresses. A single NGINX worker (worker_processes 1)
is launched with ASLR disabled via setarch x86_64 -R in the entrypoint, producing a
deterministic heap and system() address across restarts. A small Python backend
(backend.py) runs on the container's loopback, holding upstream connections open so
that spray POST bodies linger in the request pool; it is not reachable from the host.
Topology:
| Service | Role | Accessible from host |
|---|---|---|
NGINX worker (commit 98fc3bb78) |
Vulnerable HTTP listener (trigger and spray endpoints) | http://127.0.0.1:19321/ |
backend.py |
Proxy backend for /internal and /spray |
No (container-internal only) |
Download the environment files (env.zip), which include env/Dockerfile, env/docker-compose.yml, env/nginx.conf, env/entrypoint.sh, and env/backend.py.
Bringing it up:
Confirming the environment is the vulnerable one:
Once the container is running, three checks establish that this is the correct vulnerable substrate:
# 1. HTTP server is live and both attack-surface locations respond:
curl -s http://127.0.0.1:19321/ | grep -q ok && echo "nginx up"
curl -s -o /dev/null -w "api=%{http_code}\n" "http://127.0.0.1:19321/api/x" -H 'X-Delay: 0'
curl -s -o /dev/null -w "spray=%{http_code}\n" -X POST --data x http://127.0.0.1:19321/spray -H 'X-Delay: 0'
# 2. Pinned pre-fix commit and ASLR disabled:
docker exec cve-2026-42945-nginx git -C /nginx-src rev-parse HEAD
# expected: 98fc3bb78e8daef25c3d850c9cba8c2f787fb99e
docker exec cve-2026-42945-nginx bash -c 'cat /proc/$(pgrep nginx|sort -n|tail -1)/personality'
# expected: 00040000 (ADDR_NO_RANDOMIZE)
# 3. Marker directory is empty — nothing pre-planted:
docker exec cve-2026-42945-nginx ls -A /app/marker/
# expected: (empty)
Expected output: nginx up, api=200, spray=200, the commit hash above,
personality flag 00040000, and an empty /app/marker/ directory.
The is_args reset is verifiably absent: searching for the fix inside the running
source tree returns nothing, so the pre-fix code path is confirmed present.
3. How to exploit¶
Overview of the technique¶
The exploit chains two HTTP-only primitives:
-
Heap spray (POST
/spray): binary payloads containing fakengx_pool_cleanup_sstructs are POST-ed into the pool before the overflow. Each fake struct containshandler = system()(the fixed libc address, known because ASLR is off),data = <address of the command string>, andnext = 0. The command string (echo <nonce> > /app/marker/<nonce>) follows immediately in the same body. Because URI bytes cannot carry null bytes, the binary payload (including the null terminator on the command string) must arrive via POST body. -
Overflow (GET
/api/...): a single crafted GET request triggers the length/copy mismatch. The URI encodes 349 padding bytes (A), then 969 URI-escapable bytes (+), then a 6-byte target address. During the copy pass, each+expands to%2B(1 → 3 bytes), producing a ~2 000-byte overflow into the adjacentngx_pool_t. The overflow overwrites only the low 6 bytes of the adjacent pool'scleanuppointer, redirecting it to one of the sprayed fake structs. When the pool is torn down (ngx_destroy_pool), NGINX iterates the cleanup list and callshandler(data), i.e.,system("echo <nonce> > /app/marker/<nonce>"), inside the worker process.
The exploit iterates 20 pre-computed heap offset candidates × up to 10 attempts each (8 sweeps), crash-detecting between attempts via a concurrent keep-alive connection. The probabilistic nature comes from heap feng shui: the fake struct must land at exactly the offset the overwritten pointer targets.
Step-by-step¶
Download the exploit files (exploit.zip), which include exploit/run.sh and exploit/poc.py.
Step 1. Bring the environment up (see above) and verify the smoke test passes.
Step 2. Choose a per-run nonce. The nonce is the only run-specific value; it must not exist on the server before the exploit runs, and its presence after the exploit proves real command execution.
Step 3. Confirm the nonce is absent on the server (baseline check):
Step 4. Run the exploit. It is entirely HTTP-only: it opens plain TCP connections to the nginx listener and sends spray POST bodies followed by the overflow GET request. No shell, credentials, or direct filesystem access is used.
run.sh builds the command string echo $NONCE > /app/marker/$NONCE, passes it to
exploit/poc.py, and lets poc.py iterate through its offset candidates and attempt
budget (--max-rounds 8). A successful attempt typically completes within 1–2
minutes. Do not wrap the command in timeout (not present on macOS runners); the
script self-bounds.
Step 5 (teardown). When finished:
What proves it worked¶
Success is verified through an independent, privileged channel: a docker exec
shell into the running container that the exploit itself never touches. This channel
reads the server's filesystem directly; the exploit's own stdout is not the signal.
Three sub-checks must all pass:
# (a) The marker file now exists (was absent at baseline):
docker exec cve-2026-42945-nginx ls -A /app/marker/
# -> rift_33a425b86a2d
# (b) Its contents are exactly the nonce — byte-exact:
docker exec cve-2026-42945-nginx cat /app/marker/rift_33a425b86a2d
# -> rift_33a425b86a2d
# (c) It is owned by uid 65534 (nobody) — the NGINX worker's process context:
docker exec cve-2026-42945-nginx stat -c 'uid=%u gid=%g %n' /app/marker/rift_33a425b86a2d
# -> uid=65534 gid=65534 /app/marker/rift_33a425b86a2d
In this run all three checks passed with nonce rift_33a425b86a2d. The marker was
absent at baseline and appeared only after exploit/run.sh completed. The worker
uid (65534, nobody) is the process identity that executed system(), not a
host-side or root artifact. The nonce was generated by the verifier and could only
have been placed on the server by the worker executing the command string, confirming
server-side state change at the correct privilege level.
The NGINX error log also shows the overflow firing independently of the marker:
[alert] 7#0: worker process 661 exited on signal 11 (core dumped)
[notice] 7#0: start worker process 663
[alert] 7#0: worker process 663 exited on signal 11 (core dumped)
[notice] 7#0: start worker process 665
The repeated SIGSEGV, core dump, and master respawn are the expected signature of
the overflow reaching and redirecting the cleanup pointer. One of those redirected
attempts landed on a sprayed struct and called system(), producing the marker.
The worker pid advanced from baseline 11 to 665 across the attempt sequence.
The effect traces directly to the vulnerable code path: the is_args reset fix is
absent from the running build (confirmed at environment setup), and the marker can
only have been written via the ngx_destroy_pool → cleanup handler → system()
chain triggered by that missing reset.
4. Security advice¶
Remediation¶
Upgrade to a fixed release. The one-line fix (e->is_args = 0 in
ngx_http_script_regex_end_code()) is in:
- NGINX Open Source: 1.30.1 (stable branch) or 1.31.0 (mainline)
- NGINX Plus: R36 P4, R35 P2, or R32 P6
Any NGINX OSS release from 0.6.27 through 1.30.0, and NGINX Plus R32 through R36 without the relevant patch, is vulnerable to at least a worker-crash DoS from an unauthenticated HTTP request. The DoS requires no special conditions; the RCE requires ASLR to be disabled (or an independent ASLR bypass).
Workaround (if immediate upgrade is not possible)¶
Replace all unnamed PCRE captures in rewrite directives with named captures. This
prevents the three-ingredient trigger from being satisfied:
# Vulnerable pattern:
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
# Safe equivalent using a named capture:
rewrite ^/api/(?P<ep>.*)$ /internal?migrated=true;
set $original_endpoint $ep;
Named captures are referenced by $ep (the capture name), not by $1/$2, so
the copy pass does not look up a positional capture and cannot trigger the overflow.
This workaround is effective but requires auditing every location block that
combines an unnamed-capture rewrite with a ? in the replacement and a later
set/if referencing $N.
Additional mitigations¶
Enabling ASLR on the host (the Linux default) limits the practical impact to a worker crash (DoS) under currently known public techniques. The NGINX master automatically restarts the worker, so the DoS is a transient availability disruption rather than a persistent outage. That said, DoS via a single unauthenticated request is still a meaningful risk for production systems; only the patch fully addresses it.
There is no WAF-layer rule that reliably suppresses the trigger: the vulnerable URI can consist entirely of printable ASCII characters, and the three trigger ingredients are in the server configuration, not the request.
References¶
- NVD CVE-2026-42945: CVSS scores, CWE, publication date
- GHSA-gcgv-v5gf-c543: affected/fixed versions, advisory text
- F5 Security Advisory K000161019: vendor advisory (login required)
- depthfirst — NGINX Rift technical write-up: root cause analysis, ASLR notes, disclosure timeline
- depthfirst — full research note: heap feng shui,
ngx_pool_tcorruption, code execution chain - DepthFirstDisclosures/Nginx-Rift (GitHub): authoritative PoC (
poc.py,nginx.conf,Dockerfile,entrypoint.sh) - nginx patch commit 2046b45aa0c6: one-line fix:
e->is_args = 0inngx_http_script_regex_end_code() - nginx.org security advisories: not-vulnerable: 1.31.0+, 1.30.1+; vulnerable: 0.6.27–1.30.0
- The Hacker News — 18-Year-Old NGINX Rewrite Module Flaw: background and exploitation summary
- 1dayexploit.com — detailed analysis: two-pass mismatch details, ASLR section
- Help Net Security — exploited in the wild: active exploitation report