Full CVE-2026-42945 research repository with heap buffer overflow analysis, RCE exploit (heap spray + Feng Shui), detection scripts, and patching guidance for NGINX rewrite module vulnerability.
Heap Buffer Overflow in NGINX ngx_http_rewrite_module
| Metric | Value |
|---|---|
| CVSS v4.0 | 9.2 (Critical) |
| CVSS v3.1 | 8.1 (High) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | 122 — Heap-based Buffer Overflow |
| Introduced | June 2008 — v0.6.27 |
| Discovered | April 2026 — DepthFirst Research |
| Fixed | May 13, 2026 — v1.30.1, v1.31.0 |
| CVE Published | May 21, 2026 |
| Lifetime | ~18 years (undetected) |
| Fix Commit | 524977e7c534e87e5b55739fa74601c9f1102686 |
An unauthenticated, remote attacker can trigger a deterministic heap buffer overflow in NGINX worker processes by sending a crafted HTTP request to a server with a specific rewrite + set/if/rewrite configuration pattern. The overflow corrupts heap metadata (ngx_pool_cleanup_t pointers), enabling Remote Code Execution (RCE) via heap spray and Feng Shui techniques.
server {
listen 19321;
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
}
Key requirements:
rewrite directive whose replacement contains ? (query-string separator)set, if, or rewrite directive that references an unnamed PCRE capture ($1, $2, etc.)? in the rewrite replacement triggers ngx_http_script_start_args_code which sets e->is_args = 1| Capability | Description |
|---|---|
| Denial of Service | Crash worker processes deterministically, causing respawn loops (works regardless of ASLR) |
| Remote Code Execution | With ASLR disabled (or bypassed via partial overwrite), achieve full RCE as the nginx user |
| Data Exfiltration | Through memory read primitives, extract sensitive data from worker heap |
| Persistence | Plant backdoors via code execution in worker process memory |
NGINX's ngx_http_rewrite_module uses a two-pass script engine in src/http/ngx_http_script.c:
ngx_http_script_run): iterates all script codes to compute the total buffer size needed. Writes lengths to le.ip and le.pos.ngx_http_script_copy_len/_code): iterates again, writing actual bytes into the pre-allocated buffer at e->ip and e->pos.Each script code has two handlers: one for each pass. For example:
ngx_http_script_copy_len → ngx_http_script_copy_codengx_http_script_start_args_len → ngx_http_script_start_args_codeis_args FlagThe flag e->is_args on the engine structure (ngx_http_script_engine_t) controls how the copy pass handles certain characters:
typedef struct {
u_char *ip;
u_char *pos;
ngx_http_variable_value_t *sp;
ngx_str_t buf;
int flushed;
unsigned is_args:1; // <-- THE BUG
unsigned ncaptures:1;
ngx_uint_t captures_size;
// ...
} ngx_http_script_engine_t;
When e->is_args = 1, the copy-code for $N capture references calls ngx_escape_uri() with NGX_ESCAPE_ARGS, which expands:
+ → %2B (1 byte → 3 bytes, +200%)% → %25 (1 byte → 3 bytes, +200%)& → %26 (1 byte → 3 bytes, +200%)The execution flow for the vulnerable pattern:
rewrite ^/api/(.*)$ /internal?migrated=true;
? in the replacement string, which triggers ngx_http_script_start_args_code, setting e->is_args = 1.e->is_args is NEVER CLEARED.Then:
set $original_endpoint $1;
A fresh sub-engine (le) is created for the length pass:
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
This correctly zeroes le.is_args = 0, so the length pass returns the raw, unescaped capture length.
The copy pass reuses the main engine e, which still has e->is_args = 1 from step 1. The copy pass applies URI-escaping, expanding each escapable character from 1 byte to 3 bytes inside a buffer that was sized for the raw length — heap overflow.
Pass 1 (Length — sub-engine le):
le.is_args = 0
capture $1 = "A+++++B" → length = 7
Buffer allocated: 7 bytes
Pass 2 (Copy — main engine e):
e.is_args = 1 ← LEAKED from rewrite
capture $1 = "A+++++B"
ngx_escape_uri("A+++++B", NGX_ESCAPE_ARGS):
A → A (1 byte)
+ → %2B (3 bytes) ← EXPANSION
+ → %2B (3 bytes)
+ → %2B (3 bytes)
+ → %2B (3 bytes)
+ → %2B (3 bytes)
B → B (1 byte)
total written: 17 bytes
buffer size: 7 bytes
OVERFLOW: 10 bytes
The expansion ratio is 7 + (n_escapable * 2) where n_escapable is the count of +, %, and & in the capture.
| Step | Technique | Description |
|---|---|---|
| 1 | Overflow | Send crafted URI with + padding to overflow heap buffer |
| 2 | Heap Spray | POST large bodies to /spray to fill heap with controlled data |
| 3 | Feng Shui | Arrange allocations so overflow target (ngx_pool_cleanup_t) is adjacent |
| 4 | Corrupt Handler | Overflow overwrites ngx_pool_cleanup_t.handler with system() address |
| 5 | Trigger Cleanup | Wait for pool destruction → system(cmd) executes attacker command |
| 6 | Reverse Shell | Chain to reverse shell payload for interactive access |
Single-request Feng Shui fails because the overflow corrupts the pool's metadata (->d.next, ->d.failed) before reaching the cleanup pointer. When the pool is destroyed at request end, the corrupted metadata causes a crash before system() is called.
Instead, the exploit uses cross-request Feng Shui:
/spray. The backend (server.py) holds the response with X-Delay header, keeping the connection open and preserving the heap allocation. The spray fills the heap with fake ngx_pool_cleanup_t blocks.cleanup pointer (not pool metadata), pointing it to the sprayed fake block.system(cmd).