Vulnerability research

CVE-2026-42533: nginx map regex heap buffer overflow

CVE-2026-42533: a pre-auth heap buffer overflow in nginx's HTTP script engine. We reproduced the crash on 1.31.2 and verified the 1.31.3 fix end to end.

CVE-2026-42533 is a heap buffer overflow in nginx's HTTP script engine, the interpreter behind map, set, rewrite, and complex-value string expressions. The root cause is a contract failure between two passes: nginx measures an output value in a length pass, then later copies a different value into the buffer in the copy pass.

The vulnerable path is pre-auth and request-controlled. In the proven variant, URI length directly sizes the overflow. On nginx 1.31.2, that path crashes the worker. It is classified CWE-122 heap buffer overflow and scored CVSS 3.1 8.1 HIGH by F5 and CVSS 4.0 9.2 CRITICAL by NVD; the advisory notes that code execution is possible if ASLR is disabled or an attacker can bypass it.

Evidence status

CVECVE-2026-42533
nginx advisoryBuffer overflow when using map and regex
Affected0.9.6 through 1.31.2 mainline, through 1.30.3 stable
Fixed1.31.3+ mainline, 1.30.4+ stable
CWECWE-122
CVSS 3.18.1 HIGH (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
CVSS 4.09.2 CRITICAL (AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
CISA KEVNot listed as of July 20, 2026
Public working PoCNone known as of July 20, 2026
CreditMufeed VH of Winfunc Research and Maxim Dounin
01

The vulnerable path

The repro fits in a few lines because the bug is not in request parsing. It is in how nginx evaluates script variables with side effects.

map $uri $map { ~(?<capture>.*) $capture; }
server {
  listen 127.0.0.1:8081;
  location / {
    set $capture "";
    set $temp "$capture $map";
    add_header X-Temp "$temp";
    return 200 "ok\n";
  }
}
StepWhat happens
Length passnginx evaluates $capture as "", adds one space, then evaluates $map as the URI length N. It allocates N+1 bytes.
Side effectEvaluating $map runs the regex against $uri and overwrites $capture with the full URI.
Copy passnginx re-reads $capture as N bytes, adds one space, then copies $map as N bytes.
OverflowThe copy pass writes 2N+1 bytes into an N+1 byte heap allocation. The overflow is N bytes, controlled by the request URI length.

That is the useful shape of this class of bug: the first pass and second pass are both internally reasonable, but they are no longer talking about the same value.

02

The obvious fix is a trap

The tempting patch is local. Make this map non-volatile. Cap the capture length. Special-case regex-backed maps. Each of those fixes one observed path and leaves the script engine trusting an invariant it does not enforce.

Candidate fixWhy it is incomplete
Make the map value non-volatileIt narrows one timing path, but does not make copy operations safe when length and copy diverge for another reason.
Cap capture lengthIt reduces the blast radius of this repro, but still permits stale sizing if the copied value changes after measurement.
Special-case this regex capture pathIt treats map as the bug, when the bug is the engine writing without a boundary check.

The real invariant belongs at the write site. Every copy operation needs to know where the allocation ends, and it needs to stop there unconditionally.

03

The fix

The core fix in b767540492e, by Maxim Dounin, adds an e->end boundary pointer to the script engine and introduces ngx_http_script_check_length(). The guard is called from the copy operations that can write into the evaluated buffer: copy code, variable copy, regex start and end, capture copy, and complex-value copy.

If length and copy disagree, nginx now aborts the script evaluation with HTTP 500 and logs [alert] no buffer space in script copy. It does not continue writing into heap memory.

src/http/ngx_http_script.c
11 }
22  
33  
4+ngx_int_t
5+ngx_http_script_check_length(ngx_http_script_engine_t *e, size_t len)
6+{
7+ if (e->end == NULL) {
8+ return NGX_OK;
9+ }
10+ 
11+ if (e->end - e->pos < (ssize_t) len) {
12+ ngx_log_error(NGX_LOG_ALERT, e->request->connection->log, 0,
13+ "no buffer space in script copy");
14+ e->ip = ngx_http_script_exit;
15+ e->status = NGX_HTTP_INTERNAL_SERVER_ERROR;
16+ return NGX_ERROR;
17+ }
18+ 
19+ return NGX_OK;
20+}
21+ 
22+ 
423 static ngx_int_t
524 ngx_http_script_add_copy_code(ngx_http_script_compile_t *sc, ngx_str_t *value,
625 ngx_uint_t last)
726 }
827  
928 if (value && !value->not_found) {
29+ 
30+ if (ngx_http_script_check_length(e, value->len) != NGX_OK) {
31+ return;
32+ }
33+ 
1034 p = e->pos;
1135 e->pos = ngx_copy(p, value->data, value->len);

The follow-up commits carry the same boundary model through adjacent script users. 25f920eca97 extends the guard to proxy, FastCGI, SCGI, uwsgi, gRPC, index, and try_files paths. a8289aa69c7 trims the final value to the actual written length, closing a related trailing-uninitialized-bytes leak. 4d32a2703c7 applies the guard to the access-log script engine.

04

How we verified it

Assurance tested real nginx binaries built from release-1.31.2 at 2fd01ed4 and release-1.31.3 at 073ab5db, with gcc 12.2 on aarch64, AddressSanitizer, and PCRE2. The same config and the same request were used on both sides.

Evidence status

FidelityFULL
RED baselinenginx 1.31.2, release-1.31.2, 2fd01ed4
GREEN patchednginx 1.31.3, release-1.31.3, 073ab5db
Healthy control`curl /hello` returned 200 before the attack
Attack request`curl /<4000×'a'>`
RED resultConnection died, worker crashed, SIGCHLD observed
RED ASan`heap-buffer-overflow, WRITE of size 4001` at `ngx_http_script_copy_var_code`, `src/http/ngx_http_script.c:980`
GREEN resultClean HTTP 500 on all 3 repeats, worker stayed alive
GREEN log`[alert] no buffer space in script copy`
Post-attack health`curl /hello` still returned correctly after the attacks

The regression check matters because the fix sits in a shared interpreter, not in a narrow handler. A boundary guard that breaks valid interpolation would be safer than a heap overflow, but still wrong.

CheckResult
Ordinary map + regex + set configByte-identical correct output on 1.31.2 and 1.31.3
Patched nginx test suite, affected map and rewrite pathsmap.t, map_complex.t, map_volatile.t, rewrite.t, rewrite_set.t, 61 tests passed
Patched nginx test suite, adjacent script usersssi.t, proxy_variables.t, rewrite_if.t, rewrite_unescape.t, 84 tests passed
Same five affected-module tests on vulnerable 1.31.2 baselinePassed, confirming no regression introduced by the fix path
Total (script-engine-adjacent tests)145 tests, 0 regressions

Security gate result: PASS at FULL fidelity. The vulnerable binary crashed under the request-controlled overflow. The patched binary converted the same input into a contained 500 and kept serving.

05

Scope

This teardown proves the side-effect map + set variant end to end. That is the path above: regex evaluation of $map overwrites $capture between the length pass and copy pass, then set $temp "$capture $map" writes past the heap allocation.

Assurance also re-drove the volatile map variant at the byte level, inspecting the actual response bytes on both 1.31.2 and 1.31.3 rather than only checking for crash or no crash. That path is a real overflow: the divergence is about 7 bytes, and the same request trips the guard on patched 1.31.3 with HTTP 500 and [alert] no buffer space in script copy. On vulnerable 1.31.2, the write lands inside nginx's pool-allocator slack rather than across an ASan-guarded block boundary, so it does not crash the worker and does not appear in the response. Across both variants and both builds, the emitted response bytes contained no adjacent-heap content. This is confirmed as a write-overflow bug, not a read or leak, consistent with the CWE-122 heap-overflow classification and the absence of any memory-disclosure CWE for this CVE.

Two nearby nginx CVEs should stay out of this one. CVE-2026-60005 is a distinct slice module bug involving uninitialized memory access via unnamed regex captures, a genuine memory-disclosure read rather than this write-overflow path. CVE-2026-42945 is also distinct: an earlier, unrelated rewrite module bug credited to Leo Lin, fixed in 1.31.0 before CVE-2026-42533's 1.31.3 fix. It shares no code path with this fix. The b767540492e fix here touches the generic script engine, not that rewrite-module bug.

The follow-up commits extending the guard to proxy, FastCGI, SCGI, uwsgi, gRPC, index, try_files, and access-log script paths were tracked as part of the fix map, but not independently exercised in this teardown. We drove the core HTTP script path, which is where the proven crash and the proven guard behavior occur.

06

Close

The bug was a disagreement between two passes. The fix is the thing the interpreter should have enforced all along: the pass that writes is not allowed to believe the pass that measured.