The Exploit
The attacker needs no authentication or prior knowledge. The vulnerability is triggered by sending a crafted HTTP request to any NGINX instance that uses a rewrite directive with a regex pattern containing overlapping PCRE captures and a replacement string referencing multiple such captures in a redirect or arguments context.
curl -v 'http://target.com/aaa' \
-H 'Host: target.com'
When the request lands on a vulnerable NGINX instance configured with a rewrite rule such as rewrite ^/((.*))$ /redirect?param=$1$2 permanent;, the worker process will crash with a heap buffer overflow. The attacker observes a connection reset or 502 Bad Gateway response, and repeated requests can cause sustained denial of service. On systems with ASLR disabled, the overflow may be weaponized for remote code execution.
What the Patch Did
Before (vulnerable code):
if (code->lengths == NULL) {
e->buf.len = code->size;
if (code->uri) {
if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
NGX_ESCAPE_ARGS);
}
}
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += r->captures[n + 1] - r->captures[n];
}
After (fixed code):
if (code->lengths == NULL) {
e->buf.len = code->size;
cap = r->captures;
p = r->captures_data;
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += cap[n + 1] - cap[n];
if (code->uri) {
if (r->quoted_uri || r->plus_in_uri) {
e->buf.len += 2 * ngx_escape_uri(NULL, &p[cap[n]],
cap[n + 1] - cap[n],
NGX_ESCAPE_ARGS);
}
}
}
The patch moved the URI escaping buffer size calculation inside the capture iteration loop, changing it from estimating escape overhead based on the entire request URI to correctly computing it per individual capture group. This ensures the buffer allocation matches the actual data that will be escaped.
Root Cause
CWE-122: Heap-based Buffer Overflow. The dataflow begins when an HTTP request reaches an NGINX instance with a rewrite rule containing overlapping PCRE captures (e.g., ^/((.*))$). The attacker-controlled URI (/aaa) is parsed by PCRE, producing captures in r->captures_data and offsets in r->captures. The vulnerability exists in the buffer size calculation at src/http/ngx_http_script.c:1145-1153. The old code added 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len, NGX_ESCAPE_ARGS) to the buffer length using the entire request URI (r->uri.data), but the actual escaped data written to the buffer later is from individual capture groups. Since the overlapping captures (e.g., $1 and $2) contain the same underlying data, the escape overhead is double-counted relative to the actual number of characters that need escaping. This miscalculation causes the allocated buffer to be smaller than needed, leading to a heap overflow when ngx_escape_uri() is applied to each capture group during the actual buffer write phase.
Why It Works
The load-bearing single line is p = r->captures_data; combined with &p[cap[n]] inside the loop. This stores a pointer to the raw capture data once and then indexes into it per capture group, ensuring each capture's escape overhead is calculated based on its own length (cap[n + 1] - cap[n]) rather than the full URI length. If p were removed and the code fell back to using r->uri.data again, the bug would persist because the per-capture escape calculation would still reference the wrong base data. The engineer added the cap = r->captures and p = r->captures_data local variables to avoid repeated pointer dereferencing and to make the code logic clearer. The structural change—moving the URI escaping check inside the capture loop—is the defensive-in-depth addition that prevents future refactors from reintroducing the same miscalculation pattern.
Hardening Checklist
- Use
ngx_escape_uri()with per-capture group lengths rather than full URI length when calculating buffer requirements for regex captures in rewrite module code. - Validate that all buffer size calculations in
src/http/ngx_http_script.cand similar files match the actual data that will be written, not derived data like the full request URI. - Add PCRE capture group boundary assertions to ensure no overlapping captures are used in buffer allocation logic.
- Implement fuzz testing specifically for regex patterns with overlapping captures (e.g.,
^/((.*))$) combined with replacement strings referencing multiple captures. - Audit all
ngx_escape_uri()calls for parameter mismatches between the size calculation and the data pointer passed to the escape function.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-9256