Skip to content

Validate HTTP/2 request methods, targets and header fields - #545

Merged
ericmj merged 3 commits into
elixir-mint:mainfrom
bmalum:fix/http2-validate-request-fields
Oct 25, 2026
Merged

ericmj merged 3 commits into
elixir-mint:mainfrom
bmalum:fix/http2-validate-request-fields

Conversation

@bmalum

@bmalum bmalum commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Mint.HTTP2.request/5 lowercases the header names it is given and HPACK-encodes everything else unchanged, and stream_request_body/3 does the same for trailers. A method, :path, CONNECT :authority, header name or header value containing CR, LF or NUL, a header name containing a space or a colon, or a value with leading or trailing whitespace is sent to the server as is. Mint.HTTP1 rejects all of these, and Mint.HTTP2 rejects them in responses since #504, so whether a request is validated depends on which protocol ALPN negotiated.

RFC 9113 8.2.1 forbids these characters in field names and values and says a sender must not generate such a message. HTTP/2 framing isn't confused by them, but an intermediary that forwards the request over HTTP/1.1 without re-validating the fields turns an injected CRLF into header injection or request splitting on its back-end connection. An application that is safe against an HTTP/1 origin stops being safe once the origin, or a CDN in front of it, enables HTTP/2.

Requests and trailers are now validated before a stream is opened, with the field rules already used for responses: the method must be a token (RFC 9110 9.1), :path and the CONNECT :authority must be non-empty field values (RFC 9113 8.3.1), pseudo-headers passed by the caller must have valid names and values, and trailers can't carry pseudo-headers (RFC 9113 8.1). Invalid input fails with :invalid_request_method, :invalid_request_target, :invalid_header_name or :invalid_header_value, as in Mint.HTTP1; nothing is sent and the request doesn't take up a stream. The format_error/1 messages for the two header errors no longer say "in the response" since they apply to requests now too.

:path is checked with the RFC 9113 field-value rules, not the stricter HTTP/1 request-target grammar; doing the latter would need :skip_target_validation plumbing for HTTP/2 and I kept it out of this change. Header-name errors report the lowercased name.

35 tests added to conn_test.exs (invalid methods, targets, CONNECT targets, names, values, explicit pseudo-headers, trailers, and a positive case for empty values, obs-text and inner whitespace); mix test test/mint/http2/conn_test.exs 303 passed. Full mix test 697/713, the 16 failures are the docker-backed integration tests and fail identically on main here. mix format --check-formatted, --warnings-as-errors and dialyzer clean.

Prepared with AI assistance (Claude); I reproduced the CRLF pass-through against the HTTP/2 test server on main and verified the new tests fail there.

bmalum and others added 3 commits October 25, 2026 13:34
Mint.HTTP2.request/5 lowercased the header names it was given and
HPACK-encoded everything else as is, and stream_request_body/3 did the
same for trailers. A method, :path, CONNECT target, header name or
header value containing CR, LF or NUL, a header name containing a space
or a colon, or a value with leading or trailing whitespace was sent to
the server unchanged. Mint.HTTP1 rejects all of these, and Mint.HTTP2
rejects them in responses since elixir-mint#504, so the behaviour of a request
depended on which protocol ALPN negotiated.

RFC 9113 8.2.1 forbids these characters in field names and values and
says a sender must not generate such a message. HTTP/2 framing isn't
confused by them, but an intermediary that forwards the request over
HTTP/1.1 without re-validating the fields turns an injected CRLF into
request header injection or request splitting on its back-end
connection, so an application that is safe against an HTTP/1 origin
isn't once the origin or a CDN in front of it enables HTTP/2.

Requests and trailers are now validated before a stream is opened, with
the field rules already used for responses: the method must be a token
(RFC 9110 9.1), :path and the CONNECT :authority must be non-empty
field values (RFC 9113 8.3.1), pseudo-header names passed by the caller
must be valid field names, and trailers can't carry pseudo-headers (RFC
9113 8.1). Invalid input fails with :invalid_request_method,
:invalid_request_target, :invalid_header_name or :invalid_header_value,
like in Mint.HTTP1, and the connection stays open without the request
taking up a stream. The messages for the two header errors no longer
mention the response, since they now apply to requests as well.
@ericmj
ericmj merged commit 7223c00 into elixir-mint:main Oct 25, 2026
3 checks passed
@ericmj

ericmj commented Oct 25, 2026

Copy link
Copy Markdown
Member

Thank you! 💜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants