Reject requests that break the RFC 9112 request-target and Host rules - #498
Open
digitalresistor wants to merge 6 commits into
Open
Reject requests that break the RFC 9112 request-target and Host rules#498digitalresistor wants to merge 6 commits into
digitalresistor wants to merge 6 commits into
Conversation
digitalresistor
force-pushed
the
bugfix/rfc9112-request-target-and-host
branch
from
August 2, 2026 06:59
3bc8dc1 to
98ef6ef
Compare
digitalresistor
force-pushed
the
bugfix/rfc9112-request-target-and-host
branch
from
August 2, 2026 23:02
58f62ce to
ae98881
Compare
RFC 9112 section 3.2 requires a 400 (Bad Request) response to any HTTP/1.1 request that lacks a Host header field, or that carries one with an invalid field value. Waitress rejected duplicate Host header fields already, but accepted a request with none at all, and never looked at the value. The value is now checked against the "uri-host [ ':' port ]" grammar of RFC 3986 section 3.2.2 whatever the version of the request, so that a value that isn't a host can't reach the WSGI application and be interpreted differently there than it was here. Note that this means an internationalised domain name has to be punycoded by the client, as it always should have been. The existing tests that sent an HTTP/1.1 request without a Host header field have been updated to send one. See #462
RFC 9112 section 3.2.2 requires that an origin server receiving a request with an absolute-form request-target ignore the Host header field and use the host information from the request-target instead. Waitress did the opposite: it parsed the authority out of the request-target into proxy_netloc, then never looked at it again, so "GET http://evil.com/page HTTP/1.1" with "Host: victim.com" reached the application as victim.com. Anything in front of Waitress that follows the RFC would have routed that request to evil.com, and a disagreement of that shape is the basis of a host confusion attack. The authority now replaces the Host header field value, and is checked the same way a Host header field value is. That rejects a userinfo subcomponent, since "@" may not appear in a uri-host, and RFC 9110 section 4.2.1 separately requires that an "http" URI with an empty host be rejected as invalid. A CONNECT is excluded, because it uses the authority-form of request-target and urlsplit() reads that as a scheme followed by a path, which would otherwise make "CONNECT example.com:443" look like an absolute-form with an empty authority. Waitress leaves it to the WSGI application to decide what to do with a CONNECT, so its request-target is passed through untouched. A request-target that starts with "//" still parses as a path rather than as an authority, which keeps the behaviour asked for in #260. See #467
RFC 9112 section 3.2.3 requires that the request-target of a CONNECT be an authority-form, and that a server reject a CONNECT that targets an empty or invalid port number. Waitress still leaves it to the WSGI application to decide what to do with a CONNECT, up to and including implementing a proxy with it. This does not reject the method, it validates the shape of the request-target so that an application splitting a host from a port is working with something well formed rather than having to re-derive that itself. A CONNECT whose request-target is not a uri-host followed by an in-range port is now answered with a 400 (Bad Request). Note that this includes a target with no port at all, such as "CONNECT example.com", which used to be passed through. The validation runs before split_uri() sees the request-target, because urlsplit() reads "example.com:443" as the scheme "example.com" followed by the path "443". See #463
The absolute-form commit re-added a copy of the Unreleased Bugfix entries along with the whole 3.0.2 and 3.0.1 sections, so each of those releases appeared twice in the file and the 3.0.1 section grew a Bugfix block it never shipped with. Drop the duplicate. What is left is the new Unreleased section followed by the released sections exactly as they stand on main, so the branch only ever adds to the changelog.
digitalresistor
force-pushed
the
bugfix/rfc9112-request-target-and-host
branch
from
August 2, 2026 23:18
ae98881 to
7901005
Compare
RFC 9112 section 3.2 defines exactly four forms of request-target. The absolute-form and the authority-form of a CONNECT are checked by the preceding commits; the remaining two were not checked at all, so a request-target that is none of the four still reached the application. The asterisk-form is restricted by section 3.2.4 to a server-wide OPTIONS, so it is now rejected for any other method. "GET * HTTP/1.1" used to be served with a PATH_INFO of "*" for the application to puzzle over. Everything else has to be an origin-form, which section 3.2.1 defines as an absolute-path optionally followed by a query, and an absolute-path begins with a "/". "GET foo/bar HTTP/1.1" used to be served with a PATH_INFO of "foo/bar", which PEP 3333 does not allow either: a non-empty PATH_INFO starts with a slash. The check looks at the request-target as it arrived rather than at the decoded path, so a percent encoded "/" cannot stand in for a real one. A request-target beginning with "//" has no scheme and stays a path, as it already did for #260. Also retarget the unreleased section at 4.0.0, since everything in it is backward incompatible.
mmerickel
approved these changes
Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the remaining request parsing issues reported by @TUO-Wu: #462 (in part),
#463 and #467. Each is a separate commit with its own tests and changelog entry.
Require a valid Host header field on HTTP/1.1 requests
Closes #462.
RFC 9112 section 3.2 requires a 400 (Bad Request) response to any HTTP/1.1
request that lacks a Host header field, or that carries one with an invalid
field value. #484 rejected duplicate Host header fields, which is the same
sentence of the same section, but a request with no Host header field at all was
still served, and the value was never looked at:
The value is now checked against the
uri-host [ ":" port ]grammar of RFC 3986section 3.2.2, whatever the version of the request, so that something which
isn't a host can't reach the WSGI application and get interpreted differently
there than it was here.
Use the absolute-form request-target authority as the Host
Closes #467.
RFC 9112 section 3.2.2 requires that an origin server receiving a request with
an absolute-form request-target ignore the Host header field and use the host
information from the request-target instead. Waitress did the opposite: it
parsed the authority into
proxy_netlocand then never looked at it again, so$ printf 'GET http://evil.com/page HTTP/1.1\r\nHost: victim.com\r\n\r\n' | nc 127.0.0.1 8080reached the application as
victim.com. Anything in front of Waitress thatfollows the RFC would have routed that request to
evil.com, and a disagreementof that shape is the basis of a host confusion attack.
The authority now replaces the Host header field value and is validated the same
way. That rejects a userinfo subcomponent, since
@may not appear in auri-host, and RFC 9110 section 4.2.1 separately requires that an
httpURI withan empty host be rejected as invalid. A request-target starting with
//stillparses as a path rather than an authority, preserving the behaviour asked for in
#260.
Validate the authority-form request-target of a CONNECT
Closes #463.
RFC 9112 section 3.2.3 requires that the request-target of a CONNECT be an
authority-form, and that a server reject a CONNECT targeting an empty or invalid
port number.
This does not reject the method. Waitress still leaves it to the WSGI
application to decide what to do with a CONNECT, up to and including
implementing a proxy with it. What it validates is the shape of the
request-target, so that an application splitting a host from a port is working
with something well formed rather than having to re-derive that itself.
example.com:443[::1]:443example.comexample.com:0example.com:65536Where it passes through, the environ is unchanged from
mainfield for field —same
REQUEST_URI,PATH_INFOandHTTP_HOST.The validation has to run before
split_uri()sees the request-target, becauseurlsplit()readsexample.com:443as the schemeexample.comfollowed by thepath
443. For the same reason a CONNECT is excluded from the absolute-formhandling above — an authority-form is not an absolute-form, and without the
exclusion
CONNECT example.com:443would look like one with an empty authority.Backward incompatibilities
These are all cases that used to be answered with a 200, so they are called out
in CHANGES.txt:
uri-host [ ":" port ]is now a 400,at any HTTP version. In particular an internationalised domain name has to be
punycoded by the client, as it always should have been.
HTTP_HOST, and one with anempty authority or a userinfo subcomponent is a 400.
Notes
landed in Reject duplicate Host headers per RFC 9112 #484 and Reject requests with duplicate Content-Type #488, so the first commit backfills those.
field and needed one added; that churn is contained in the Issues in Parsing HTTP Request "Host" Header #462 commit.
parser.pyandrfc7230.pyare both at 100% coverage.