CVE-2026-42533: NGINX Heap Overflow via Non-Cacheable Variable Re-Evaluation
Overview
| CVE | CVE-2026-42533 |
| Advisory | F5 K000162097 |
| Component | src/http/ngx_http_script.c (ngx_http_script_regex_start_code()) |
| Affected | NGINX Open Source ≤ 1.31.2 / 1.30.0-1.30.3; NGINX Plus 37.0.0.1-37.0.2.1 (Rx R33-R36); NGINX Instance Manager, F5 WAF for NGINX, NGINX App Protect WAF, NGINX Gateway Fabric, NGINX Ingress Controller (bundled base components) |
| Fixed | NGINX Open Source 1.31.3 / 1.30.4; NGINX Plus 37.0.3.1 / R36 P7 |
| Class | CWE-122 (Heap-Based Buffer Overflow) |
| CVSS (F5-assigned, full advisory) | 8.1 High (CVSS v3.1) / 9.2 Critical (CVSS v4.0) |
| Found by | Lukas Johannes Moeller (@JohannesLks) -- one of several researchers F5 credits for independently reporting this issue |
TL;DR
The rewrite directive evaluates NOCACHEABLE variables twice per request: once to size the replacement buffer, once to fill it. If the variable returns a longer string on the second call, the copy overflows the heap allocation. This is the sister bug of F5 case 01159216, which patched the identical double-evaluation flaw in the set/if path (ngx_http_rewrite_value()) but left the rewrite directive's own script engine untouched. F5's advisory bundles this with a related trigger through the map directive's regex capture variables, reported independently by other researchers in the same coordinated disclosure.
Root Cause
ngx_http_script_regex_start_code() runs a length pass with a zeroed sub-engine:
// src/http/ngx_http_script.c:1164
le.ip = code->lengths->elts;
le.line = e->line;
le.request = r;
le.quote = code->redirect;
// le.flushed is 0 (ngx_pcalloc)
len = 0;
while (*(uintptr_t *) le.ip) {
lcode = *(ngx_http_script_len_code_pt *) le.ip;
len += lcode(&le); // evaluates NOCACHEABLE via ngx_http_get_flushed_variable()
}
The length pass calls ngx_http_get_flushed_variable() because le.flushed == 0. That function resets valid = 0 and re-evaluates the variable. The buffer is then allocated at line 1185 based on the length-pass result.
After allocation, the outer rewrite engine e (also flushed == 0, set by ngx_pcalloc in ngx_http_rewrite_handler() at ngx_http_rewrite_module.c:161) runs the copy codes. These call ngx_http_get_flushed_variable() a second time, re-evaluate the NOCACHEABLE variable, and ngx_cpymem at ngx_http_script_copy_var_code (line 980) writes the new, larger value past the allocated buffer.
The fix for case 01159216 patched ngx_http_rewrite_value() (the set/if code path). The rewrite directive's ngx_http_script_regex_start_code() path was not included in that earlier fix -- it is what this report covers.
Trigger
Any rewrite rule referencing a NOCACHEABLE variable in the replacement string:
location /rewrite-test {
rewrite ^/rewrite-test/(.*)$ /target/$1?p=$nocache_var last;
}
Built-in NOCACHEABLE variables include $connections_active, $msec, and $request_time. In a multi-worker deployment under load, $connections_active regularly crosses string-length boundaries (e.g. 99 to 100), triggering the overflow without any attacker-controlled input beyond a normal HTTP request.
Advisory scope vs. this write-up
F5's published description for CVE-2026-42533 leads with a different trigger: a map directive using regex matching where a string expression references the map's regex capture variables before referencing the map output variable. It then adds, "alternatively, the same result could be achieved by using a non-cacheable variable in a string expression under certain conditions" -- that second sentence is the double-evaluation bug documented on this page and reported as the sister of case 01159216. F5 acknowledges more than a dozen researchers by name for this advisory, so the map-directive path was almost certainly found independently by someone else; this write-up only speaks to the NOCACHEABLE path with a verified root cause, live proof, and a working fix.
Proof
nginx 1.31.2 built from upstream commit 1c387799 with AddressSanitizer. A deterministic NOCACHEABLE test module returns 4096 bytes on the first evaluation and 8192 bytes on the second, guaranteeing the overflow in a single request:
$ curl -s "http://127.0.0.1:18096/nocache-rewrite/AAAA..."
==107138==ERROR: AddressSanitizer: memcpy-param-overlap:
memory ranges [0x52500000390a,0x52500000590a) and
[0x525000005100, 0x525000007100) overlap
#0 memcpy
#2 ngx_http_script_copy_var_code src/http/ngx_http_script.c:980
#3 ngx_http_rewrite_handler src/http/modules/ngx_http_rewrite_module.c:180
0x52500000490b is located 0 bytes after 8203-byte region
allocated by:
#4 ngx_http_script_regex_start_code src/http/ngx_http_script.c:1185
Buffer allocated for 8203 bytes (length pass: 8 + 4096 + 3 + 4096). Copy pass writes 12299 bytes (8 + 4096 + 3 + 8192) -- a 4096-byte overflow. The error log confirms the same request pointer evaluated the variable twice:
nocache_poc variable call=1 len=4096 request=0000521000010550
nocache_poc variable call=2 len=8192 request=0000521000010550
Impact
An unauthenticated request that happens to hit a digit-boundary crossing on a built-in NOCACHEABLE variable (or a crafted request against a custom NOCACHEABLE variable) overflows the heap allocation, crashing the nginx worker process -- a remote, pre-auth denial of service. F5's assigned score for the full advisory (8.1/9.2) also accounts for the map-directive path, which is more directly attacker-controlled than the timing-dependent NOCACHEABLE trigger documented here; this write-up's own narrower assessment for the double-evaluation path alone is CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H (5.9, Medium).
Fix
Set le.flushed = 1 on the length sub-engine and e->flushed = 1 on the outer engine after allocation. Both passes then use ngx_http_get_indexed_variable(), which returns the cached value instead of re-evaluating:
--- a/src/http/ngx_http_script.c
+++ b/src/http/ngx_http_script.c
@@ -1167,6 +1167,7 @@
le.line = e->line;
le.request = r;
le.quote = code->redirect;
+ le.flushed = 1;
len = 0;
@@ -1189,6 +1190,8 @@
return;
}
+ e->flushed = 1;
+
e->is_args = 0;
e->quote = code->redirect;
Disclosure
Reported to F5 SIRT as the sister of case 01159216 shortly after F5's fix for that case shipped without covering the rewrite path. F5 confirmed and tracked the issue internally as NPS-72, folded it into a broader advisory alongside independently-reported issues in the same script-engine area, and published CVE-2026-42533 / K000162097 on July 15, 2026 with coordinated public disclosure.