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

FieldValue
Primary CWECWE-122: Heap-based Buffer Overflow
Related CWECWE-131: Incorrect Calculation of Buffer Size
Related CWECWE-126: Buffer Over-read (heap data leaks into HTTP response)

3. Severity

CVSS 3.1

SourceScoreVector
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 ALAS7.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 GroupMetricValue
Base — ExploitabilityAttack Vector (AV)Network
Attack Complexity (AC)High
Attack Requirements (AT)Present
Privileges Required (PR)None
User Interaction (UI)None
Base — Vulnerable SystemConfidentiality (VC)High
Integrity (VI)High
Availability (VA)High
Base — Subsequent SystemConfidentiality (SC)None
Integrity (SI)None
Availability (SA)None
FieldValue
CVSS 4.0 Score9.2 (Critical)
CVSS 4.0 VectorCVSS: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

ProductAffected VersionsFixed Version
NGINX Open Source (mainline)0.1.17 through 1.31.01.31.1
NGINX Open Source (stable)0.1.17 through 1.30.11.30.2
NGINX PlusR37 (37.0)R37 P1 (37.0.1.1)
NGINX PlusR36R36 P5
NGINX PlusR32R32 P7
ProductCPE 2.3
NGINX Open Sourcecpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:* (versions < 1.31.1)
NGINX Pluscpe:2.3:a:f5:nginx_plus:*:*:*:*:*:*:*:*

Tested Environment (Vulnerable)

FieldValue
SoftwareNGINX Open Source 1.31.0
Built fromgithub.com/nginx/nginx tag release-1.31.0
PlatformAlpine Linux (container), x86_64
Source filesrc/http/ngx_http_script.c
Compiled withgcc, default flags

Tested Environment (Patched)

FieldValue
SoftwareNGINX Open Source 1.31.1
Built fromgithub.com/nginx/nginx tag release-1.31.1
PlatformAlpine Linux (container), x86_64
Fix commitsca4f92a (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:

  1. code->size — the static size of the replacement string template (everything except capture references)
  2. 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 ^/((.*))$:

  • $1 captures all 500 + chars, $2 captures the same 500 + chars
  • Buffer allocates escape overhead for one pass: 2 * 500 = 1000 extra bytes
  • Actual write escapes each capture separately: 2 * 500 * 2 = 2000 extra bytes needed
  • Overflow: ~1000 bytes past the allocated buffer

Additionally, commit 475732a fixes two related issues:

  1. e->is_args was not reset at the start of rewrite execution — a stale flag from a prior rewrite could cause the escape budget to be calculated incorrectly
  2. le.is_args was not copied from e->is_args in ngx_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)

FileDescription
poc_cve_2026_9256.pyPython 3 PoC script — triggers crash (DoS) and probes for heap info leak (ASLR bypass)
nginx-vuln.confVulnerable 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 rewrite directive using overlapping PCRE captures in redirect or query-string context

Setup:

  1. Build nginx 1.31.0 from the release-1.31.0 tag:

    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 install
    
  2. Write 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;
            }
        }
    }
    
  3. Start nginx: nginx

Trigger:

  1. Run the PoC:

    python3 poc_cve_2026_9256.py <target_ip> 80
    

    Or 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

Metricnginx 1.31.0 (vulnerable)nginx 1.31.1 (patched)
Requests sent (crash test, 500 +)510
Worker crashes (signal 11)5/5 (100%)0/10 (0%)
Worker PID stableNo (changed each request)Yes (same PID throughout)
Heap data in response (30 +)Yes — 49 bytes of pool pointers leakedNo
NUL bytes in response headersYes (10-50 char payloads)No
Error log: “exited on signal 11”Yes, every 500-char requestNo 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:

  1. Abnormally long URIs with high density of + or %-encoded characters
  2. Repeated connection resets from the server (worker crash → RST) following such requests
  3. 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
RuleMatch Means
CVE_2026_9256_Vulnerable_Nginx_Binarynginx 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

SourceURL
F5/NGINX Advisoryhttps://my.f5.com/manage/s/article/K000161377
NGINX Security Advisorieshttps://nginx.org/en/security_advisories.html
MITRE CVEhttps://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.1https://github.com/nginx/nginx/releases/tag/release-1.31.1
ReporterMufeed VH of Winfunc Research