The Exploit
An attacker needs only network access to send an HTTP/2 request to an NGINX proxy configured with proxy_http_version 2 and proxy_set_body. No authentication is required.
HEADERS (HTTP/2) - stream 1
:method = POST
:path = /
:scheme = http
:authority = target.com
content-length = 0
CONTINUATION (HTTP/2) - stream 1
x-ignore: ignored
DATA (HTTP/2) - stream 1
[0x00 0x00 0x05 0x00 0x01 0x00 0x00 0x00 0x01 0x68 0x65 0x6c 0x6c 0x6f]
When the proxy forwards this request upstream with proxy_set_body enabled, the upstream server receives a malformed frame header followed by attacker-controlled payload data. The attacker can inject arbitrary DATA frames into the upstream connection, potentially sending false response data or poisoning the upstream stream state.
Upstream receives:
0x00 0x00 0x05 0x00 0x01 0x00 0x00 0x00 0x01 0x68 0x65 0x6c 0x6c 0x6f
What the Patch Did
Before
if (plcf->body_values) {
f = (ngx_http_proxy_v2_frame_t *) b->last;
b->last += sizeof(ngx_http_proxy_v2_frame_t);
f->length_0 = (u_char) ((body_len >> 16) & 0xff);
f->length_1 = (u_char) ((body_len >> 8) & 0xff);
f->length_2 = (u_char) (body_len & 0xff);
f->type = NGX_HTTP_V2_DATA_FRAME;
f->flags = NGX_HTTP_V2_END_STREAM_FLAG;
f->stream_id_0 = 0;
f->stream_id_1 = 0;
f->stream_id_2 = 0;
f->stream_id_3 = 1;
e.ip = plcf->body_values->elts;
e.pos = b->last;
e.request = r;
e.flushed = 1;
e.skip = 0;
while (*(uintptr_t *) e.ip) {
code = *(ngx_http_script_code_pt *) e.ip;
code((ngx_http_script_engine_t *) &e);
}
b->last = e.pos;
}
After
if (plcf->body_values) {
// Removed the creation of DATA frame here
}
The patch removes the entire block that generated a forged HTTP/2 DATA frame when proxy_set_body was active. Instead of producing a raw DATA frame header followed by the body content, the fixed code now passes the body through NGINX's normal chunked transfer encoding mechanism, which correctly separates HEADERS and DATA frames.
Root Cause
CWE-122: Heap-based Buffer Overflow. The vulnerability occurs because the code constructs a raw HTTP/2 DATA frame header directly from attacker-controlled body_len without validating that the body length matches the actual content produced by the script engine. When proxy_set_body is configured with a value that evaluates to a different size than body_len (e.g., using NGINX variables like $request_body or $http_cookie), the frame header declares a payload length that doesn't match the real payload. The dataflow: attacker sends an HTTP/2 request with a body; NGINX evaluates proxy_set_body values using the script engine, calculating body_len from the original request; the code then creates a DATA frame header claiming body_len bytes, writes the script output to b->last, but the actual script output may contain additional frame headers from the attacker's original request. When the upstream connection receives this malformed stream, it interprets the excess data as new frames, allowing frame injection.
Why It Works
The single load-bearing line is the removal of the entire if (plcf->body_values) block that created the raw DATA frame. Without this removal, even if other defensive lines were added, the fundamental flaw remains: the code treats attacker-controlled bytes as part of NGINX's own frame construction. The engineer could have added body_len validation or a boundary check, but the correct fix is to never allow attacker data to be interpreted as frame headers. The other changes in the patch (moving the body_len calculation and cl->next allocation to a new else if (body_len) branch) are necessary for correctness—they ensure the proxy forwards the body through the standard HTTP/2 framing path rather than constructing its own frames.
Hardening Checklist
- Use
ngx_http_validate_length()to verify that all frame lengths in incoming requests match the actual payload before passing data to script evaluation. - Avoid constructing raw protocol frames from user-controlled data; use NGINX's built-in framing APIs (
ngx_http_v2_filter_get_data_frame()) instead of memory manipulation. - Apply
ngx_pnalloc()with size validation when allocating frame header buffers to prevent writing beyond allocated space if script output exceeds declared length. - Implement
ngx_http_v2_state_complete()state transitions to ensure upstream connections reject unsolicited frames that attempt to inject into established streams. - Enable
proxy_request_buffering onto buffer entire request bodies before forwarding, preventing the proxy from passing partially received frames to the upstream.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-42926