Skip to content

gRPC request serialization overflows on oversized HPACK :path #28

@xintenseapple

Description

@xintenseapple

Summary

ngx_http_grpc_create_request() underestimates the outgoing HTTP/2 request HEADERS buffer when serializing a client-controlled :path of length NGX_HTTP_V2_MAX_FIELD + 1. The undersized request-pool temp buffer is later expanded in-place for CONTINUATION frames with ngx_memmove(), causing a heap out-of-bounds write.

This is the gRPC sibling of the proxy_v2 issue tracked in trailofbits/ptp-nginx#11: the root HPACK length-reservation mistake is the same, but the affected module, directives, and stack are ngx_http_grpc_module.c / grpc_pass.

ASAN demonstrates a 135-byte OOB write starting exactly at the end of the allocated buffer. In production builds this is still an out-of-bounds heap write during request construction. Practical impact is at least worker crash / denial of service; exploitability beyond crash was not assessed.

Affected Area

  • Component: HTTP gRPC upstream request serialization
  • Function: ngx_http_grpc_create_request()
  • File: src/http/modules/ngx_http_grpc_module.c
  • Key anchors:
    • fixed HPACK length reserves: :745, :765, :774, :804-805, :838-839
    • temp buffer allocation: :853-857
    • actual path serialization: :942-944 / :966-975
    • CONTINUATION insertion OOB write: :1120
  • HPACK constants/writer: src/http/v2/ngx_http_v2.h:23-25, src/http/v2/ngx_http_v2_encode.c:23-59

Impact

A remote client can trigger heap corruption when the target location uses grpc_pass, accepts very large request targets via large_client_header_buffers, and has low incidental gRPC-header slack. The backend does not need to be malicious; the crash happens while nginx constructs the upstream request, before the backend accepts a connection.

The minimal accounting error at the boundary is one byte, but the observed overflow is not a one-byte overwrite. The later CONTINUATION-frame memmove() writes a larger block past the buffer; ASAN reports WRITE of size 135.

Expected Behavior

The gRPC request serializer should reject over-limit HPACK string sources or reserve the exact number of bytes needed for the encoded HPACK integer length.

Actual Behavior

The serializer reserves NGX_HTTP_V2_INT_OCTETS bytes for HPACK string lengths. NGX_HTTP_V2_INT_OCTETS is 4, and NGX_HTTP_V2_MAX_FIELD is 2,097,278. A raw HPACK string length of 2,097,278 fits in four integer bytes, but 2,097,279 emits a fifth byte. ngx_http_grpc_create_request() does not reject that value and still allocates using the four-byte estimate.

After serializing the HEADERS block, nginx inserts CONTINUATION frame headers in place with ngx_memmove(). Because the serialized HEADERS block is larger than the estimate, that write crosses the request-pool temp-buffer boundary.

Steps to Reproduce

Use an ASAN build and a location like:

http {
    large_client_header_buffers 64 2200k;
    server {
        listen 127.0.0.1:23220;
        location / {
            grpc_set_header Host "";
            grpc_set_header TE "";
            grpc_set_header Content-Length "";
            grpc_set_header Connection "";
            grpc_pass grpc://127.0.0.1:23221;
        }
    }
}

Run any TCP listener on 127.0.0.1:23221, then send a request target of length 2,097,279 bytes to the nginx frontend. The control case at 2,097,278 bytes is ASAN-clean.

import socket
uri_len = 2097279
path = "/" + ("~" * (uri_len - 1))
req = (f"GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").encode()
s = socket.create_connection(("127.0.0.1", 23220))
s.sendall(req)
print(s.recv(1024))
s.close()

Evidence

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 135
    #0 memmove
    #1 ngx_http_grpc_create_request src/http/modules/ngx_http_grpc_module.c:1120
    #2 ngx_http_upstream_init_request src/http/ngx_http_upstream.c:655
    #3 ngx_http_upstream_init src/http/ngx_http_upstream.c:568
    #4 ngx_http_read_client_request_body src/http/ngx_http_request_body.c:103
    #5 ngx_http_grpc_handler src/http/modules/ngx_http_grpc_module.c:603

0 bytes after 2098511-byte region
allocated by thread T0 here:
    #1 ngx_create_temp_buf src/core/ngx_buf.c:22
    #2 ngx_http_grpc_create_request src/http/modules/ngx_http_grpc_module.c:857

Test matrix:

Case Input Result
control :path request target length 2,097,278 ASAN clean
trigger :path request target length 2,097,279 ASAN heap-buffer-overflow before backend accept

Additional oversized method/header cases were clean in the harness because HPACK Huffman encoding or incidental name/header slack masked the one-byte under-reserve. The path case demonstrates the broken allocation invariant directly.

Suggested Fix

Apply request-side NGX_HTTP_V2_MAX_FIELD validation in ngx_http_grpc_create_request() for method, path, authority, configured grpc_set_header values, and pass-through request headers. The response-side HTTP/2 filter already uses this style of max-field check before relying on the fixed NGX_HTTP_V2_INT_OCTETS reserve.

Alternatively, replace fixed NGX_HTTP_V2_INT_OCTETS reservations with exact encoded HPACK integer-length accounting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions