RC RANDOM CHAOS

NGINX ships emergency patch for HTTP/3 heap overflow

CVE-2026-42945 technical analysis: heap overflow in NGINX HTTP/3 HEADERS frame parsing, worker RCE primitive, telemetry gaps, and patch boundary.

· 7 min read

CVE-2026-42945. NGINX. CVSS v3 base score 9.1. Heap-based buffer overflow in the HTTP/3 request header processing path, reachable pre-authentication over UDP/443 against any worker with QUIC enabled. CWE-122. Affected versions span NGINX 1.25.0 through 1.27.3 with the experimental QUIC module compiled in, and NGINX Plus R31 through R32 prior to patch level 1. Vendor advisory confirms in-the-wild exploitation observed against edge proxies and CDN origin nodes. Initial reports describe worker process crashes. Subsequent analysis confirms the primitive is sufficient for remote code execution in the worker context under realistic heap conditions.

The bug lives in the QUIC frame reassembly path during HEADERS frame decoding. NGINX’s HTTP/3 implementation buffers fragmented HEADERS frames across multiple QUIC STREAM frames before handing the reassembled byte sequence to the QPACK decoder. The buffer sizing calculation trusts a length descriptor derived from the variable-length integer encoded in the frame header. A signed comparison on that integer fails to reject values that, after a downstream cast to size_t, produce a wrap. The allocation request becomes small. The copy that follows uses the original attacker-supplied length, written into the worker’s heap pool. Classic length confusion, classic outcome. The pool allocator’s metadata sits adjacent to the target buffer, and the overflow walks straight into it.

The pool is NGINX’s per-request slab. It is not glibc’s ptmalloc and it is not jemalloc. ngx_pool_t maintains a singly-linked list of large allocations and a bump allocator for small ones. Overwriting the next pointer of a pool large allocation node turns the next call to ngx_pool_cleanup_add or ngx_destroy_pool into a controlled write or a controlled call, depending on which cleanup handler fires first. The cleanup callbacks are function pointers stored in pool metadata. An attacker who controls the heap layout controls which callback runs and what argument it receives. From there the primitive becomes a one-shot function pointer hijack inside the worker, executing with the worker’s UID - typically nginx or www-data - and with whatever capabilities the worker inherits from the master process. On most distributions that means CAP_NET_BIND_SERVICE retained, no CAP_SYS_PTRACE, and an AppArmor or SELinux profile that may or may not be enforcing.

Reaching the condition does not require authentication, a valid Host header, or a routable upstream. The vulnerable code path executes during the QUIC handshake completion and the first HEADERS frame of the first stream. The TLS layer terminates inside NGINX’s QUIC stack, so the bytes the parser sees are the decrypted contents of a QUIC short header packet. The attacker needs UDP reachability to port 443 and a QUIC-capable client. The shape of the malicious request is a single QUIC initial packet followed by a 0-RTT or 1-RTT packet carrying a malformed HEADERS frame. No application-layer state is required. The primitive is reached before any rewrite rules, before any access module, before any auth_request subrequest. WAF rules that operate on parsed HTTP semantics never see the payload.

Exploitation in the wild is consistent with a single threat cluster operating against exposed reverse proxies fronting SaaS tenancies. Observed behaviour to date: crash-only payloads used as reconnaissance to enumerate vulnerable hosts, followed by targeted RCE attempts against a subset. The RCE payloads dropped to disk are minimal - a Go-compiled reverse shell in /tmp, persistence via a cron entry under the nginx user, and outbound TLS to infrastructure previously associated with initial access brokers selling proxied access into corporate networks. MITRE ATT&CK mapping is T1190 for initial access, T1059.004 for the Unix shell stage, T1053.003 for cron persistence, and T1071.001 for command and control over HTTPS. No kernel-mode component. No persistence in the NGINX binary itself. The worker is the foothold, not the objective.

What defenders see depends on what they are collecting. The crash-only variant produces an unambiguous signal in the NGINX error log: a worker process %d exited on signal 11 line, repeated at the rate of attacker requests. The worker auto-respawns under the master, so service availability is preserved and the events are easily lost in noise on high-traffic edges. On Linux, auditd records the segfault if audit rules cover SIGSEGV delivery, and the kernel logs a segfault at hex_address ip hex_rip sp hex_rsp error code line to dmesg. If systemd-coredump is enabled, a core file lands in /var/lib/systemd/coredump with the worker’s memory image - useful for retrospective triage, dangerous if attacker-controlled bytes including session keys are captured and not access-restricted.

On Windows builds of NGINX, which are less common in production but present in some appliance images, the equivalent telemetry is Windows Error Reporting events and Sysmon Event ID 5 for the worker process termination. EDR coverage of NGINX worker crashes is uneven. CrowdStrike Falcon, SentinelOne, and Defender for Endpoint will surface the process termination but rarely classify it as exploitation absent a behavioural follow-on. The follow-on is what generates the high-confidence alert: the spawned shell, the outbound connection, the file write to /tmp. Sysmon Event ID 1 for a /bin/sh or /bin/bash child of nginx is the canonical detection. NGINX workers do not legitimately spawn shells. A correlation rule joining parent_image ending in nginx with child_image of any shell binary fires with near-zero false positives in a properly segmented production environment.

Network telemetry is harder. QUIC traffic is encrypted end-to-end and the vulnerable bytes sit inside the encrypted payload. Suricata and Zeek can identify QUIC flows on UDP/443 and extract the initial packet’s SNI in the clear, but the HEADERS frame contents are unreachable to inline inspection unless the sensor terminates TLS - which most do not. Detection at the network layer therefore relies on flow-level anomalies: an unusual rate of single-packet QUIC connections from a narrow source range, connections that complete the handshake and immediately RST or vanish, or geographic and ASN patterns inconsistent with the site’s legitimate user base. None of these are reliable in isolation. The detection of value is the host-side crash-plus-child-process correlation.

Where defenders are blind: the heap groom. Before the overflow lands, the attacker shapes the worker pool by sending a sequence of benign-looking requests that allocate and free large pool nodes in a predictable pattern. From the outside, those requests are valid HTTP/3 traffic with normal headers and normal sizes. Access logs record them as 200s or 404s. Nothing about them is anomalous individually. The groom is invisible in any telemetry that does not include heap allocator instrumentation, and no production NGINX deployment runs with that instrumentation. The first observable event in the attack chain is the crash itself. Everything before it is lost.

Patch boundary is NGINX 1.27.4 and NGINX Plus R32 P1. The fix is a corrected bounds check on the QUIC HEADERS frame length descriptor and an added explicit cast guard before the pool allocation request. The patch is small and isolated to ngx_http_v3_parse.c. Backporting is straightforward for organisations on long-term-support distribution packages where the maintainer has not yet shipped the update. Upstream NGINX advised that disabling the HTTP/3 listener - removing the listen 443 quic directive - fully mitigates the issue without touching the binary. Workers that do not bind QUIC sockets cannot reach the vulnerable code path. For deployments that require HTTP/3, restricting UDP/443 ingress to a known CDN front via security group or firewall reduces the exposed attack surface to the CDN’s edge, which in most cases is itself patched on a faster cadence than origin infrastructure.

Residual exposure after patch is non-zero for two reasons. First, the worker’s heap may already have been groomed and the crash may already have been logged before the patch was applied - retrospective hunting on error logs for signal 11 entries dated before the patch window is the only way to know whether the host was probed. Second, third-party builds - appliance firmware, container images pinned to a tagged minor version, and any distribution that vendors NGINX into a larger product - inherit the vulnerability and the patch timeline of the vendor, not the upstream project. F5’s NGINX Plus customers are on the documented R32 P1 path. Everyone running an OEM build is on the OEM’s path. Inventory the actual binary version, not the package metadata. nginx -V output is authoritative. The presence of —with-http_v3_module in the compile flags determines whether the code path is reachable. Without that flag, the worker is not vulnerable regardless of version.

The operational reality is that this CVE will produce two populations of compromised hosts. The first is the set that crashed visibly and was investigated. The second is the set that crashed, respawned, and was never looked at because the error log line scrolled past in a 10,000-request-per-second access pattern. The second set is larger. The hunt query is trivial. Run it.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.