Skip to content

Reject requests that break the RFC 9112 request-target and Host rules - #498

Open
digitalresistor wants to merge 6 commits into
mainfrom
bugfix/rfc9112-request-target-and-host
Open

Reject requests that break the RFC 9112 request-target and Host rules#498
digitalresistor wants to merge 6 commits into
mainfrom
bugfix/rfc9112-request-target-and-host

Conversation

@digitalresistor

@digitalresistor digitalresistor commented Aug 2, 2026

Copy link
Copy Markdown
Member

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:

$ printf 'POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n' | nc 127.0.0.1 8080
HTTP/1.1 200 OK

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 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_netloc and 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 8080

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 validated the same
way. 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 request-target starting with // still
parses 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.

request-target before after
example.com:443 pass-through pass-through
[::1]:443 pass-through pass-through
example.com pass-through 400, no port
example.com:0 pass-through 400, invalid port
example.com:65536 pass-through 400, invalid port

Where it passes through, the environ is unchanged from main field for field —
same REQUEST_URI, PATH_INFO and HTTP_HOST.

The validation has to run before split_uri() sees the request-target, because
urlsplit() reads example.com:443 as the scheme example.com followed by the
path 443. For the same reason a CONNECT is excluded from the absolute-form
handling above — an authority-form is not an absolute-form, and without the
exclusion CONNECT example.com:443 would 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:

  • An HTTP/1.1 request without a Host header field is now a 400.
  • A Host header field whose value is not a 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.
  • An absolute-form request-target now determines HTTP_HOST, and one with an
    empty authority or a userinfo subcomponent is a 400.
  • A CONNECT whose request-target is not a valid authority-form is now a 400.

Notes

@digitalresistor
digitalresistor force-pushed the bugfix/rfc9112-request-target-and-host branch from 3bc8dc1 to 98ef6ef Compare August 2, 2026 06:59
@digitalresistor digitalresistor changed the title Reject requests that break the RFC 9112 request-target and Host rules Require a valid Host header field and honour the absolute-form request-target Aug 2, 2026
@digitalresistor digitalresistor changed the title Require a valid Host header field and honour the absolute-form request-target Reject requests that break the RFC 9112 request-target and Host rules Aug 2, 2026
@digitalresistor
digitalresistor force-pushed the bugfix/rfc9112-request-target-and-host branch from 58f62ce to ae98881 Compare August 2, 2026 23:02
The fixes themselves landed in #484 and #488, but neither added a
CHANGES.txt entry, so a reader of the changelog had no way to find out
that duplicate Host, Content-Length and Content-Type header fields are
now rejected.
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
digitalresistor force-pushed the bugfix/rfc9112-request-target-and-host branch from ae98881 to 7901005 Compare August 2, 2026 23:18
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.
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.

Incorrect Handling of Absolute-Form Request-Target Authority Parsing issue of the CONNECT method Issues in Parsing HTTP Request "Host" Header

2 participants