If you're running NGINX with rewrite and set directives in your config — and millions of deployments do — there's a critical heap buffer overflow vulnerability that's been sitting in your web server since 2008. Dubbed NGINX Rift, this flaw was autonomously discovered by the depthfirst security research system in April 2026 and allows an unauthenticated attacker to achieve Remote Code Execution (RCE) on the server.
Let's break down exactly what happened, how the bug works, how it's exploited, and what you need to do right now.
Background: Why NGINX Matters
NGINX powers nearly a third of all websites globally. It's the undisputed king of high-performance web serving — handling static files, reverse proxying, load balancing, and acting as the critical edge layer for countless backend systems. When a vulnerability lands in NGINX, it doesn't just affect one target. It potentially affects hundreds of millions of deployments.
The depthfirst research team aimed their autonomous code analysis system at the NGINX source code. Six hours later, it surfaced five memory corruption issues, four of which were subsequently confirmed by NGINX:
| CVE | Severity | CVSS | Component | Type |
|---|---|---|---|---|
| CVE-2026-42945 | Critical | 9.2 | ngx_http_rewrite_module | Heap Buffer Overflow → RCE |
| CVE-2026-42946 | High | 8.3 | ngx_http_scgi_module, ngx_http_uwsgi_module | Excessive Memory Allocation → Crash |
| CVE-2026-40701 | Medium | 6.3 | ngx_http_ssl_module | Use After Free |
| CVE-2026-42934 | Medium | 6.3 | ngx_http_charset_module | Out-of-Bounds Read |
The star of the show is CVE-2026-42945 — the heap overflow. The researchers went all the way and built a working proof-of-concept demonstrating unauthenticated RCE. Let's walk through it from root cause to shell.
The Directives at the Center of It All
To understand the bug, you need to understand the two NGINX configuration directives that trigger it: rewrite and set.
The rewrite Directive
rewrite lets you modify a request's URI on the fly using regular expressions. A classic use case is API migration:
rewrite ^/api/(.*)$ /v2/api/$1;
Here, the part matched in parentheses — a capture group — is stored as $1 and appended to the new path. Critically, if the replacement string contains a question mark, NGINX treats everything after it as a query string and appends the original request arguments to it.
The set Directive
set assigns a value to a custom NGINX variable. It's commonly used to save parts of the original request before a rewrite transforms the URI:
set $original_endpoint $1;
This saves the first capture group from the most recently executed regex into $original_endpoint. Together, these directives are standard building blocks in API gateway and reverse proxy configurations.
Under the Hood: NGINX's Two-Pass Script Engine
NGINX doesn't naively execute these directives at runtime. Instead, at configuration load time, the script engine compiles the directives into a sequence of operations. At request time, it executes them in a two-pass process:
- Pass 1 — Length Calculation: Walk through all the script operations and calculate the exact total number of bytes needed for the output string.
- Pass 2 — Copy: Allocate a buffer of exactly that size from the memory pool, then execute the copy operations to write data into it.
This design is elegant and efficient — it avoids many small heap allocations. But it rests on a critical assumption: the state of the script engine must be identical in both passes. If anything changes between them, the size calculated in Pass 1 won't match the data written in Pass 2.
That mismatch is exactly what CVE-2026-42945 exploits.
The Root Cause: A State Flag That Never Resets
The flaw lives in src/http/ngx_http_script.c.
The is_args Flag Gets Set
When a rewrite directive's replacement string contains a question mark (e.g., /internal?migrated=true), NGINX calls ngx_http_script_start_args_code:
void
ngx_http_script_start_args_code(ngx_http_script_engine_t *e)
{
e->is_args = 1; // <-- permanently set on the main engine
e->args = e->pos;
e->ip += sizeof(uintptr_t);
}
The is_args flag is now permanently set to 1 on the main script engine e. It is never reset between directive evaluations.
The set Directive Uses a Fresh Sub-Engine for Length Calculation
When the subsequent set $original_endpoint $1 directive executes, it calls ngx_http_script_complex_value_code. For the length calculation pass, this function spins up a completely zeroed-out sub-engine called le:
void
ngx_http_script_complex_value_code(ngx_http_script_engine_t *e)
{
ngx_http_script_engine_t le;
...
ngx_memzero(&le, sizeof(ngx_http_script_engine_t)); // all zeros
le.ip = code->lengths->elts;
...
Because le is freshly zeroed, le.is_args is 0.
The Length Calculation Takes the Wrong Branch
The length function ngx_http_script_copy_capture_len_code checks whether to account for URI escaping:
size_t
ngx_http_script_copy_capture_len_code(ngx_http_script_engine_t *e)
{
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// Account for escape expansion: each escapable char becomes 3 bytes
return cap[n + 1] - cap[n]
+ 2 * ngx_escape_uri(NULL, &p[cap[n]], cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
} else {
return cap[n + 1] - cap[n]; // <-- just the raw byte count
}
}
Since le.is_args == 0, the condition is false. The engine falls through to the else branch and returns the raw, unescaped capture length — say, N bytes.
The Copy Pass Runs on the Main Engine (Where is_args == 1)
During the copy pass, ngx_http_script_copy_capture_code runs on the original main engine e, where is_args is still 1:
void
ngx_http_script_copy_capture_code(ngx_http_script_engine_t *e)
{
if ((e->is_args || e->quote)
&& (e->request->quoted_uri || e->request->plus_in_uri))
{
// This branch IS taken now, because e->is_args == 1
// OVERFLOW: buffer is N bytes, but this writes N + 2*K bytes
e->pos = (u_char *) ngx_escape_uri(pos, &p[cap[n]],
cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
} else {
e->pos = ngx_copy(pos, &p[cap[n]], cap[n + 1] - cap[n]);
}
}
Now the condition is true. ngx_escape_uri is called with NGX_ESCAPE_ARGS, which expands every URI-special character (like +, &, =, %) from 1 byte to 3 bytes. If the captured URI data contains K escapable characters, the write size is N + 2*K bytes.
The buffer was only allocated for N bytes.
The result: a heap buffer overflow of up to 2*K bytes, where K is entirely controlled by the attacker through the URI they send.
The Vulnerable Configuration Pattern
Any NGINX config that combines a query-string rewrite with a set referencing a capture group is vulnerable:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
Exploitation: From Overflow to Shell
The researchers didn't stop at the crash. They built a working RCE proof of concept. Here's how the exploit works.
Attacker Advantage: Deterministic Heap Layout
NGINX uses a multi-process architecture. A master process forks worker processes to handle requests. Because of fork(), the entire memory layout of every worker is a byte-for-byte copy of the master. This means:
- The heap layout is identical across all workers.
- If an exploit attempt crashes a worker, the master spawns a new one with the exact same layout.
- Attackers can attempt the exploit repeatedly without losing their "target map" — effectively giving them unlimited crash-and-retry.
This is a huge advantage for exploitation. In theory, this could even be leveraged to brute-force ASLR byte-by-byte. The researchers demonstrated their PoC with ASLR disabled, but they note the bypass path exists.
The Target: ngx_pool_t Cleanup Handlers
NGINX uses memory pools (ngx_pool_t) to manage allocations per-connection and per-request. The pool structure looks like this:
struct ngx_pool_s {
ngx_pool_data_t d; // offset 0
size_t max; // offset ~40
ngx_pool_t *current; // offset ~48
ngx_chain_t *chain; // offset ~56
ngx_pool_large_t *large; // offset ~60
ngx_pool_cleanup_t *cleanup; // offset 64 <-- TARGET
ngx_log_t *log;
};
The cleanup field points to a linked list of cleanup handlers that NGINX calls when the pool is destroyed:
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler; // function pointer
void *data; // argument to pass
ngx_pool_cleanup_t *next;
};
If an attacker can overwrite the cleanup pointer to point at a fake ngx_pool_cleanup_s structure — with handler pointing to system() and data pointing to a command string — then when NGINX destroys that pool, it executes the attacker's command.
The Obstacle: URI-Safe Bytes Only
The heap overflow is triggered through the URI, which goes through NGINX's URI parser and ngx_escape_uri. This means the overflow bytes are restricted to URI-safe characters — no null bytes, no arbitrary binary. This makes injecting real pointers (which often contain null bytes) directly into the overflow impossible.
The exploit solves this with two clever techniques.
Technique 1: Heap Feng Shui for Timing Control
To overwrite the cleanup pointer, the attacker must first overwrite all the preceding pool fields (d, max, current, chain, large). Writing garbage to those fields will corrupt the allocator. If the victim connection tries to allocate memory after this corruption, NGINX will crash prematurely — before the exploit completes.
The solution is to destroy the pool immediately after corruption, before anything else touches those fields. The researchers call this "cross-request heap feng shui":
- Open Connection A and send partial HTTP headers. NGINX allocates a request pool for this connection.
- Open Connection B (the victim). NGINX allocates a victim pool directly adjacent to A's pool on the heap.
- Complete Connection A's headers, triggering the
rewrite/setoverflow. The overflow erupts out of A's pool and into B's adjacent pool header, corrupting it — including thecleanuppointer. - Immediately close Connection B. NGINX calls
ngx_destroy_poolon the now-corrupted victim pool.
During pool destruction, NGINX iterates the cleanup linked list — but doesn't touch the other corrupted fields like current or large. The exploit survives the cleanup traversal and triggers the handler call.
Technique 2: Heap Spray via POST Bodies
Now for getting real pointers into memory. POST request bodies — unlike headers and URIs — are treated as raw byte streams. They can contain null bytes and arbitrary binary data.
The attackers spray the heap by sending many POST requests whose bodies contain carefully crafted fake ngx_pool_cleanup_s structures:
[fake handler ptr = &system()][fake data ptr = &"cmd_string"][fake next = NULL]
Because the heap layout is deterministic, these fake structures land at predictable, fixed offsets. The attacker finds (by brute force or analysis) a heap address for the spray region that, when written as bytes, consists entirely of URI-safe characters (no null bytes, no special chars). That address is then used as the overflow value to overwrite the cleanup pointer.
The final flow:
- Spray heap with POST bodies containing fake cleanup structures.
- Perform the heap feng shui to corrupt victim pool's
cleanuppointer with the spray address. - Close the victim connection.
- NGINX calls
ngx_destroy_pool→ iteratescleanuplist → callshandler(data)→ executessystem("your_command_here").
Shell obtained. No authentication required.
Affected Versions
This bug was introduced in 2008 and affects a massive range of software:
| Product | Affected Versions |
|---|---|
| NGINX Open Source | 0.6.27 through 1.30.0 |
| NGINX Plus | R32 through R36 |
| NGINX Instance Manager | 2.16.0 through 2.21.1 |
| F5 WAF for NGINX | 5.9.0 through 5.12.1 |
| NGINX App Protect WAF | 4.9.0–4.16.0 and 5.1.0–5.8.0 |
| F5 DoS for NGINX | 4.8.0 |
| NGINX App Protect DoS | 4.3.0 through 4.7.0 |
| NGINX Gateway Fabric | 1.3.0–1.6.2 and 2.0.0–2.5.1 |
| NGINX Ingress Controller | 3.5.0–3.7.2, 4.0.0–4.0.1, 5.0.0–5.4.1 |
Are You Vulnerable?
You are potentially at risk if your NGINX configuration contains both:
- A
rewritedirective whose replacement string contains a question mark (making it set query string args), and - A
setdirective that follows it and references a regex capture group ($1,$2, etc.)
And the incoming request URI contains characters that would be escaped by NGX_ESCAPE_ARGS (such as +, &, =, etc.).
Search your nginx configs:
grep -rn "rewrite\|set \$" /etc/nginx/
Look for patterns like:
rewrite ^/something/(.*)$ /other?param=value; # has a ?
set $var $1; # references capture group
What To Do
- Update immediately. Apply the patches released by F5/NGINX on May 13, 2026. Check the official NGINX security advisory for patched versions.
- Audit your configs. Review all NGINX configs for the
rewrite+setpattern described above. If you can restructure the logic to avoid using both in sequence, do so as a belt-and-suspenders measure.
- Enable ASLR on your servers. The published PoC was demonstrated with ASLR off. Real-world exploitation with ASLR on is harder (though not theoretically impossible given NGINX's deterministic multi-worker layout).
- Monitor worker crashes. Unusual NGINX worker process crashes or respawns could indicate active exploitation attempts. Check
/var/log/nginx/error.logfor worker restart patterns.
- WAF/IDS rules. Consider adding detection for URIs with large numbers of escape-triggering characters (
+,&,%XXsequences) in paths matched by your rewrite rules.
Key Takeaways
- The bug is 18 years old. CVE-2026-42945 was introduced in NGINX 0.6.27, released in 2008. It sat undetected through countless audits.
- The trigger is common configuration.
rewriteandsettogether are standard patterns in API gateways — this isn't some obscure edge case. - The exploit is elegant. It chains a state-propagation bug through a two-pass engine design, bypasses URI-safety restrictions with POST body spraying, and uses NGINX's own multi-worker resilience as an exploit primitive.
- Patch now. With a working public PoC available, exploitation in the wild is a matter of when, not if.
Based on the original research by Zhenpeng (Leo) Lin at depthfirst, published May 13, 2026. The proof-of-concept source code is available at github.com/depthfirstdisclosures/nginx-rift.
Thanks for reading.