feat(http1): expose configurable request-header parser admission limits (#993) - #1000
Aditya-9-6 wants to merge 3 commits into
Conversation
…ts (cloudflare#993) Expose max_header_size and max_headers on HttpServerOptions, propagating them through ServerSession down to HttpSession in HTTP/1. Motivation: Previously, HTTP/1 request header parsing enforced hardcoded limits of 1,048,575 bytes (~1 MiB) and 256 headers. Reverse proxies needing stricter limits to mitigate DoS or buffer bloat were forced to buffer the full ~1 MiB before rejecting requests at the filter stage. Key changes: - HttpServerOptions: Added max_header_size and max_headers fields with validate() checking for non-zero bounds. - ServerSession & HttpSession: Added getters and setters for both limits. - HttpSession::read_request: Dynamically bounds initial buffer capacity, guards against buffer bloat before reading, restricts httparse header slices to max_headers with zero heap allocations, and validates parsed byte length upon completion. - Connection reuse: Propagates limits across keepalive sessions in HttpServerApp::process_new. - Comprehensive unit tests covering size limits (single chunk, multi-chunk, pipelined prefix), header count limits, HttpServerOptions validation, and default limit preservation. Closes cloudflare#993
|
CWL downstream handoff refresh after current-head repair Protected/released Pingora 0.9.0 remains the earlier #1000 current exact head is Current-source re-review shows all seven CWL findings reported across the earlier candidate lineage are repaired at mutable candidate scope only:
The tests cover exact-limit complete success, exact-limit incomplete immediate rejection/no-extra-read, multi-read remaining-budget rejection, exact-limit multi-read success, one-large-field/many-small-fields limits, direct setter bounds, and pipelined current-header/suffix preservation. Exact-current COMMENT review The remaining promotion boundary is therefore maintainer review/integration into protected Pingora, a later release-qualified supplier identity containing the capability, and then unchanged downstream #72 parser/application/origin GREEN. CWL will not pin this mutable contributor PR, weaken the parser oracle, add a callback-only 431 workaround, or claim release/cutover credit from candidate CI alone. |
seonghobae
left a comment
There was a problem hiding this comment.
Downstream exact-head technical review: the current patch has a parser-admission false rejection on the prebuffered/pipelined-prefix path in addition to the configuration/resource findings already recorded in the owner-path comment. read_request() assigns already_read = pipelined_prefix.len() and checks already_read > max_header_size before parsing the current request. Existing Pingora pipelining semantics allow that prefix to contain the current request plus body and/or following requests; therefore a within-limit current header can be rejected solely because buffered suffix bytes make the prefix exceed the configured header limit. Acceptance should parse/measure the current header independently of buffered suffix bytes, preserve the suffix, and add both positive (within-limit current header + large buffered suffix) and negative (genuinely over-limit current header) regressions. This is a COMMENT review only, not approval or release-authority credit.
seonghobae
left a comment
There was a problem hiding this comment.
One additional API-invariant issue remains after the six findings already recorded in the owner-path comment. The patch exposes ServerSession::set_max_header_size / set_max_headers and the underlying HttpSession setters as public, infallible methods. Those setters directly assign the values and do not run HttpServerOptions::validate(). Therefore, even if HttpServerOptions::validate() is wired into service activation, a supported public session path can still install Some(0), raise max_header_size above MAX_HEADER_SIZE, or pass max_headers > MAX_HEADERS (which is then silently clamped in read_request()).
Because #993 explicitly allows a supported per-connection/session option as an alternative API shape, validation needs to be invariant across every public mutation path, not only the HttpServerOptions path. A repair can make these setters fallible and enforce the same bounds, restrict them to crate-internal validated propagation, or otherwise make invalid states unrepresentable before read_request(). Please add direct-setter regressions for zero and above-supplier-max values if the public setters remain part of the API.
COMMENT only; this does not grant approval or release-authority credit.
…alidation - Measure and parse HTTP/1 headers in read_request() independently of buffered suffix bytes in pipelined prefixes, avoiding premature false rejection when suffix data (body / next requests) exceeds max_header_size. - Ensure suffix bytes are preserved in preread_body for body processing and subsequent pipelined requests. - Enforce invariant bounds validation across all public mutation paths: make ServerSession and HttpSession setters fallible, rejecting 0 or values exceeding MAX_HEADER_SIZE (1,048,575) / MAX_HEADERS (256). - Update HttpServerOptions::validate() with the same upper bounds checks. - Add positive and negative pipelining regression tests, direct-setter tests, and HttpServerOptions upper bounds tests.
abdaf23 to
67c6ece
Compare
|
Thanks for the thorough review and catch on the pipelined prefix admission and setter validation paths, @seonghobae! I have pushed updates addressing both findings: 1. Pipelining / Prebuffered Suffix False Rejection
2. API Invariant & Setter Bounds Validation
All unit tests and |
seonghobae
left a comment
There was a problem hiding this comment.
Current exact-head re-review of 67c6eceab0e6ccab75993100ce88d5cec279aabc confirms that the two findings from my prior reviews were materially repaired at candidate scope: the pipelined-prefix path now measures the completed current header independently of a large buffered suffix and preserves that suffix, with positive/negative regressions; and the public ServerSession/HttpSession setters are now fallible and share zero/upper-bound validation. The current build and Semgrep runs are also GREEN.
Three promotion-blocking gaps remain before this can serve as downstream parser-admission authority:
- Fail-closed activation is still missing. In
pingora-core/src/apps/mod.rs, the actual H1 new-session and reused-session paths apply both fallible setters withlet _ = .... The shown activation path does not callHttpServerOptions::validate()and discards setter errors, so an invalid operator value such as zero orMAX+1can be silently ignored and the session can continue with its default limit instead of failing configuration/service activation. Please make activation propagate/validate these errors and add a regression proving invalidHttpServerOptionscannot start/serve an H1 session. - The socket read itself is not bounded by the remaining header budget. The parser computes the configured maximum and rejects after reads, but
read_request_buf()is still entered without an explicit remaining-byte cap. The patch therefore has no regression proving a single read cannot consume/grow the buffer beyond the remaining admission budget before rejection. Please bound the read by the remaining budget (while preserving parser progress semantics) and add a multi-read near-limit/over-limit resource regression. - Exact-budget incomplete headers still receive another read opportunity.
HeaderParseState::Partialrejects only whenbuf.len() > max_header_size. At exactlybuf.len() == max_header_size, a complete header can no longer grow without violating the configured budget, yet the code breaks to the outer loop and may callread_request_buf()again. Please rejectPartialat equality without another socket read/inactivity wait, while retaining exact-limitComplete(s)success.
These are semantic/resource-bound findings despite the current GREEN upstream checks. COMMENT only; this is not approval or release-authority credit.
…budget partial rejection
|
Thanks for the keen re-review and precise feedback, @seonghobae! I have pushed commit 1. Fail-Closed Service & Session Activation
2. Socket Read Bounded by Remaining Header Budget
3. Immediate Rejection on Exact-Budget Incomplete Headers
All unit tests and |
seonghobae
left a comment
There was a problem hiding this comment.
Current exact-head re-review of 6a90c79b61fbbc70b518709de6802165668cba2c confirms that the three findings from my prior 67c6ece... review are now materially repaired at candidate scope, while the four earlier repaired invariants remain preserved.
HttpServerApp::process_new()now callsHttpServerOptions::validate()before protocol/session activation, and both new and reused H1 paths propagate the fallible header-limit setters instead of discarding errors. Invalid zero/MAX+1 operator values therefore fail closed in the shown activation path.HttpSession::read_request_buf()now receives the configured byte ceiling, computes the remaining budget, rejects a zero remainder, and wraps the underlying stream withtake(remaining as u64), so the socket read itself is bounded by the remaining request-header budget rather than merely rejecting after over-read.HeaderParseState::Partialnow rejects atbuf.len() >= max_header_size, while the completed path permits an exact-limitComplete(s)and rejects onlys > max_header_size. The added regressions cover exact-limit complete success, exact-limit incomplete immediate failure/no-extra-read, remaining-budget multi-read rejection, and exact-limit multi-read success.
The earlier bounds, header-count fail-closed validation, pipelined current-header-versus-suffix accounting/suffix preservation, and direct public setter validation regressions are still present. I did not find a new actionable semantic/resource-bound defect in the current four-file range.
Current Semgrep run 34434779521 is GREEN. Exact-head build 34434779481 is still executing, so this source review does not grant execution GREEN, approval, maintainer-integration, or release-authority credit. Downstream ContextualWisdomLab/pingora-gateway#72 must remain RED/Draft until a maintainer-integrated and later release-qualified supplier identity contains this capability and the unchanged real-socket acceptance turns GREEN.
|
Thanks for the keen re-review and precise feedback, @seonghobae! I have pushed commit 6a90c79 addressing all three findings:
|
seonghobae
left a comment
There was a problem hiding this comment.
Follow-up exact-head execution closure for 6a90c79b61fbbc70b518709de6802165668cba2c: all four current checks are now terminal GREEN — Rust 1.85.0, Rust 1.97.1, nightly, and Semgrep. I re-checked the current four-file range after execution closure; the seven downstream-reported parser-admission invariants remain materially repaired at candidate scope and I found no new actionable semantic/resource-bound defect. This remains contributor-candidate evidence only: COMMENT, not approval, maintainer-integration, or release-authority credit. Downstream ContextualWisdomLab/pingora-gateway#72 should stay RED/Draft until this capability is maintainer-integrated into protected source and later appears in a release-qualified supplier identity, then the unchanged real-socket acceptance can be rerun.
Description
This PR addresses #993 by exposing configurable request-header admission limits (
max_header_sizeandmax_headers) onHttpServerOptions, propagating them throughServerSessionto the underlying HTTP/1HttpSession.Motivation
In HTTP/1.x downstream servers, Pingora previously enforced hardcoded limits:
MAX_HEADER_SIZE: 1,048,575 bytes (~1 MiB)MAX_HEADERS: 256 headersFor reverse proxies and security-sensitive gateways, rejecting oversized headers only in downstream application filters (e.g.
request_filter) is often too late: the server has already buffered up to 1 MiB of untrusted header bytes into memory per connection. Enforcing configurable admission bounds directly inHttpSession::read_request()prevents memory exhaustion and slowloris-style buffer bloat before request processing begins. Conversely, proxies operating in trusted environments can adjust limits appropriately.Key Changes
HttpServerOptions:pub max_header_size: Option<usize>(bytes limit).pub max_headers: Option<usize>(header count limit).validate(&self) -> Result<()>checking that neither limit is set toSome(0).Session Layer Accessors:
set_max_header_size,max_header_size,set_max_headers, andmax_headersonServerSessionandHttpSession.Zero-Allocation Parser Admission (
HttpSession::read_request):INIT_HEADER_BUF_SIZE.min(max_header_size).MAX_HEADER_SIZEguard withalready_read > max_header_size.&mut headers[..max_headers]when constructinghttparse::Request. This enforces the count limit on the stack with zero heap allocations, returningInvalidHTTPHeadervia httparse'sTooManyHeaders.if s > max_header_sizeinHeaderParseState::Complete(s)to prevent large single-chunk reads or pipelined prefixes from exceeding limits.Keep-Alive Preservation:
server_options.max_header_sizeandserver_options.max_headersacross reused connections inHttpServerApp::process_new.Backward Compatibility:
None. When unset, existing defaults (MAX_HEADER_SIZE = 1,048,575bytes andMAX_HEADERS = 256) remain strictly intact.Testing
Added comprehensive unit tests covering:
test_max_header_size_within_limit: Valid request within custom size limit parses successfully.test_max_header_size_exceeded_multi_chunk: Request exceeding limit across multiple chunks is rejected withInvalidHTTPHeader.test_max_header_size_exceeded_single_chunk: Complete request exceeding limit in a single read is rejected.test_max_header_size_pipelined_prefix: Pipelined prefix exceeding limit is rejected.test_max_headers_within_limit: Header count within custom limit succeeds.test_max_headers_exceeded: Exceeding header count limit rejects withInvalidHTTPHeader.test_default_limits_preserved: Default configuration preserves existing behavior.test_http_server_options_validation: Validates non-zero bounds on options.Closes #993