Tighten four RFC 9112 rules that Waitress did not enforce - #503
Open
digitalresistor wants to merge 1 commit into
Open
Tighten four RFC 9112 rules that Waitress did not enforce#503digitalresistor wants to merge 1 commit into
digitalresistor wants to merge 1 commit into
Conversation
Validate the HTTP version. RFC 9112 section 2.3 asks a server to answer 505 (HTTP Version Not Supported) when the major version is one it does not support, and to process a higher minor version within a major it does implement as the highest minor it is conformant with. Waitress accepted any [0-9].[0-9] and kept it verbatim. That is more than untidiness: everything version dependent is keyed on the version being exactly "1.0" or "1.1", so an unrecognised one fell through all of it and a request claiming HTTP/2.0 had its Connection and Expect header fields silently ignored. Reject obsolete line folding. RFC 9112 section 5.2 requires a server to either reject an obs-fold or replace it with SP before interpreting the field value; Waitress did neither, joining the continuation onto the line before it and keeping the original whitespace. That leaves us reading a field one way while something in front of us that rejects folding, or unfolds differently, reads it another. Folding has been deprecated since RFC 7230 in 2014. Drop the header fields named in a request's Connection field before building the environ. RFC 9110 section 7.6.1 makes them connection specific, applying to a single hop and not to be forwarded. Waitress is the end of the connection, so handing them to the application amounts to forwarding: a client can name a header there that a proxy in front of us believes only it controls, and the application cannot tell the two apart. Skip only empty lines before the request-line. RFC 9112 section 2.2 sanctions ignoring a CRLF there, which lets a client send a spare one after a request when pipelining. A bare lstrip() also ate spaces, tabs, vertical tabs, form feeds and lone CR or LF octets, none of which may appear before a request-line, and swallowing them hid them from the checks below. The request-line version in tests/test_parser.py was HTTP/8.4 in 25 places, chosen to show the parser did not care about it. It does now, so those use a version we support.
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.
Four independent findings from an RFC 9112 audit, all backward incompatible, so the changelog section is retargeted at 4.0.0. Independent of #498 and #502.
Validate the HTTP version, answer 505
RFC 9112 section 2.3 asks a server to answer 505 when the major version is one it does not support, and to process a higher minor version within a major it does implement as the highest minor it is conformant with. Waitress accepted any
[0-9].[0-9]and kept it verbatim:This is more than untidiness. Everything version dependent is keyed on the version being exactly
1.0or1.1, so an unrecognised one fell through all of it — a request claimingHTTP/2.0had itsConnectionandExpectheader fields silently ignored.HTTP/1.2now normalises to1.1and honoursConnection: close, which it previously did not.The version-less
GET /form that the request-line regex still permits is left alone here; that is a separate decision.Reject obsolete line folding
RFC 9112 section 5.2 requires a server to either reject an obs-fold with 400 or replace it with SP before interpreting the field value. Waitress did neither, joining the continuation onto the preceding line and keeping the original whitespace:
became
HTTP_X_FOO = "bar\tTransfer-Encoding: chunked".Waitress itself was not desynchronised by this — it never saw a
Transfer-Encoding— but it left us reading a field one way while a front end that rejects folding, or unfolds it differently, read it another. Folding has been deprecated since RFC 7230 in 2014.Drop
Connection-named header fields from the environRFC 9110 section 7.6.1 makes the field names listed in
Connectionconnection specific: they apply to a single hop and must not be forwarded. Waitress is the end of the connection, so handing them to the application amounts to forwarding them.used to arrive as
environ["HTTP_X_SECRET"] == "leaked". A client can name a header there that a proxy in front of Waitress believes only it controls, and the application has no way to tell the two apart.Skip only empty lines before the request-line
RFC 9112 section 2.2 sanctions ignoring a CRLF received before a request-line, which lets a client send a spare one after a request when pipelining. Waitress used a bare
lstrip(), which also ate spaces, tabs, vertical tabs, form feeds and lone CR or LF octets — none of which may appear there, and swallowing them hid them from the checks below.Test churn
tests/test_parser.pyusedHTTP/8.4as its request-line version in 25 places, precisely to show the parser did not care. It does now, so those use a version we support. The five tests that pinned obs-fold being unfolded now pin it being rejected.Verification
Full default
toxenvlist green:lint,py39throughpy314,pypy39,pypy310,pypy311,coverageat its--fail-under=100gate, anddocs.Note for review
connection_options()inrfc7230.pyis also added by the pending security branch, which ships first. When that lands onmainthis branch will need a rebase, and the duplicate addition resolves by keeping a single copy.