NGINX RIFT: AN 18-YEAR-OLD BUG THAT CAN HAND ATTACKERS YOUR SERVER
WEB APPLICATION SECURITY

NGINX Rift: An 18-Year-Old Bug That Can Hand Attackers Your Server

researcher
Naveen Jagadeesan
published
2026-05-29
platform
Web Application

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:

CVESeverityCVSSComponentType
CVE-2026-42945Critical9.2ngx_http_rewrite_moduleHeap Buffer Overflow → RCE
CVE-2026-42946High8.3ngx_http_scgi_module, ngx_http_uwsgi_moduleExcessive Memory Allocation → Crash
CVE-2026-40701Medium6.3ngx_http_ssl_moduleUse After Free
CVE-2026-42934Medium6.3ngx_http_charset_moduleOut-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:

  1. Pass 1 — Length Calculation: Walk through all the script operations and calculate the exact total number of bytes needed for the output string.
  2. 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:

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":

  1. Open Connection A and send partial HTTP headers. NGINX allocates a request pool for this connection.
  2. Open Connection B (the victim). NGINX allocates a victim pool directly adjacent to A's pool on the heap.
  3. Complete Connection A's headers, triggering the rewrite/set overflow. The overflow erupts out of A's pool and into B's adjacent pool header, corrupting it — including the cleanup pointer.
  4. Immediately close Connection B. NGINX calls ngx_destroy_pool on 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:

  1. Spray heap with POST bodies containing fake cleanup structures.
  2. Perform the heap feng shui to corrupt victim pool's cleanup pointer with the spray address.
  3. Close the victim connection.
  4. NGINX calls ngx_destroy_pool → iterates cleanup list → calls handler(data) → executes system("your_command_here").

Shell obtained. No authentication required.

Affected Versions

This bug was introduced in 2008 and affects a massive range of software:

ProductAffected Versions
NGINX Open Source0.6.27 through 1.30.0
NGINX PlusR32 through R36
NGINX Instance Manager2.16.0 through 2.21.1
F5 WAF for NGINX5.9.0 through 5.12.1
NGINX App Protect WAF4.9.0–4.16.0 and 5.1.0–5.8.0
F5 DoS for NGINX4.8.0
NGINX App Protect DoS4.3.0 through 4.7.0
NGINX Gateway Fabric1.3.0–1.6.2 and 2.0.0–2.5.1
NGINX Ingress Controller3.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:

  1. A rewrite directive whose replacement string contains a question mark (making it set query string args), and
  2. A set directive 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

  1. Update immediately. Apply the patches released by F5/NGINX on May 13, 2026. Check the official NGINX security advisory for patched versions.
  1. Audit your configs. Review all NGINX configs for the rewrite + set pattern described above. If you can restructure the logic to avoid using both in sequence, do so as a belt-and-suspenders measure.
  1. 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).
  1. Monitor worker crashes. Unusual NGINX worker process crashes or respawns could indicate active exploitation attempts. Check /var/log/nginx/error.log for worker restart patterns.
  1. WAF/IDS rules. Consider adding detection for URIs with large numbers of escape-triggering characters (+, &, %XX sequences) in paths matched by your rewrite rules.

Key Takeaways

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.