Feature or enhancement
Summary
http.server.BaseHTTPRequestHandler does not enforce three RFC 7230
framing-validation rules. The module's docstring already states it is
not intended for production, and PSRT (via Stan Ulbrych and Seth
Larson, 2026-05-26) suggested filing these as a public RFC-compliance
improvement rather than as security issues, so this issue collects them
in one place for a single hardening PR. The three defects are
independently reproducible on main (commit 776573c) and on the
3.13 / 3.14 maintenance branches; each one is a single-rule deviation
that can be fixed in BaseHTTPRequestHandler without changing the
public API.
Defect A — duplicate Content-Length with conflicting values is accepted
RFC 7230 §3.3.3 rule 4 requires the recipient to reject a message that
has multiple Content-Length field-values that disagree (MUST close
the connection and respond 400 Bad Request). BaseHTTPRequestHandler
calls http.client.parse_headers and never inspects
headers.get_all("Content-Length"), so a request such as
POST / HTTP/1.1
Host: 127.0.0.1
Content-Length: 4
Content-Length: 0
ABCD
is dispatched to the handler. A handler that reads
int(self.headers.get("Content-Length")) silently sees only the first
value (4), while a different handler that takes get_all and picks
the last value sees 0. The behaviour is implementation-dependent
even within the standard library, which is the exact situation RFC
7230 §3.3.3 was written to prevent.
- file:
Lib/http/server.py
- function:
BaseHTTPRequestHandler.parse_request
- observed: request accepted; handler dispatched
- expected:
400 Bad Request + Connection: close
Defect B — Transfer-Encoding header is accepted but never decoded
RFC 7230 §3.3.3 rule 3 states that if a Transfer-Encoding header is
present, it overrides Content-Length and the recipient must dechunk
the body. BaseHTTPRequestHandler does not implement a chunked
decoder, but it also does not reject the header, so a request such as
POST / HTTP/1.1
Host: 127.0.0.1
Transfer-Encoding: chunked
Content-Length: 5
0
GET /pwn HTTP/1.1
Host: 127.0.0.1
is accepted and a handler that reads int(Content-Length) bytes
leaves the rest of the chunked frame (and the trailing request) in the
socket buffer, so the keep-alive loop parses GET /pwn HTTP/1.1 as a
new top-level request line. Per RFC 7230 §3.3.3 the correct response
is 400 Bad Request + close, because the server cannot honour the
Transfer-Encoding semantics it received.
- file:
Lib/http/server.py
- function:
BaseHTTPRequestHandler.parse_request
- observed: request accepted; handler dispatched; framing desynchronised
- expected:
400 Bad Request + Connection: close
Defect C — request body is not drained between requests on a persistent connection
RFC 7230 §6.3 requires the server to either consume the entire
request body or close the connection before reading the next request
on a persistent connection. BaseHTTPRequestHandler.handle_one_request
does neither: after do_<method>() returns, the next iteration of
handle() calls rfile.readline(65537) directly. A request such as
GET / HTTP/1.1
Host: 127.0.0.1
Content-Length: 100
AAAAAAAAAAAAAAAA...AAAGET /pwn HTTP/1.1
Host: 127.0.0.1
results in SimpleHTTPRequestHandler (which does not read the body of
a GET) leaving 100 bytes plus the smuggled request line in the
buffer. The keep-alive loop then reads
AAAA...AAAAGET /pwn HTTP/1.1 as a malformed request line and replies
501 Unsupported method ('AAAA...GET'), generated entirely from
attacker-controlled input.
- file:
Lib/http/server.py
- function:
BaseHTTPRequestHandler.handle_one_request / handle
- observed: leftover body parsed as next request line
- expected: leftover body drained (within a sane cap) or connection closed
Reachability
http.server is documented as not for production, but
BaseHTTPRequestHandler is the basis for wsgiref.simple_server,
which is used in many quick-start guides, development servers
(manage.py runserver style flows, Flask app.run in some
configurations, plenty of python -m http.server tutorials), CI
fixtures, internal admin tools and embedded devices. RFC 7230 framing
rules are the foundation of HTTP request-boundary integrity; honouring
them does not change semantics for any well-formed client and only
costs three header checks plus a bounded body drain.
Reproduction
Three self-contained reproductions (one per defect) are available on
request; each starts a ThreadingHTTPServer on 127.0.0.1, sends the
raw bytes shown above over socket.socket, and prints the observed
response.
Suggested fix
A single PR can address all three defects in BaseHTTPRequestHandler:
- In
parse_request, after http.client.parse_headers returns,
reject requests with conflicting Content-Length values (defect A).
- In
parse_request, after the same point, reject any
Transfer-Encoding header (defect B) — once http.server grows a
chunked decoder this check can be narrowed to "anything other than
identity".
- In
handle_one_request, wrap self.rfile with a small
byte-counting reader for the duration of the request, then after
do_<method>() returns drain up to a bounded number of bytes of
any unread declared body (defect C). If the declared body exceeds
the drain cap, close the connection instead.
All three checks are conservative: well-formed clients are unaffected.
Linked PR
A pull request implementing the fix and adding regression tests in
Lib/test/test_httpservers.py plus a Misc/NEWS.d entry is being
prepared.
Acknowledgement
Per PSRT (Stan Ulbrych, 2026-05-26 14:51 UTC and Seth Larson,
2026-05-26 21:55 UTC) these defects are out of scope for the security
process and were invited to be filed as a public issue plus PR.
Reported by tonghuaroot.
Linked PRs
Feature or enhancement
Summary
http.server.BaseHTTPRequestHandlerdoes not enforce three RFC 7230framing-validation rules. The module's docstring already states it is
not intended for production, and PSRT (via Stan Ulbrych and Seth
Larson, 2026-05-26) suggested filing these as a public RFC-compliance
improvement rather than as security issues, so this issue collects them
in one place for a single hardening PR. The three defects are
independently reproducible on
main(commit776573c) and on the3.13 / 3.14 maintenance branches; each one is a single-rule deviation
that can be fixed in
BaseHTTPRequestHandlerwithout changing thepublic API.
Defect A — duplicate
Content-Lengthwith conflicting values is acceptedRFC 7230 §3.3.3 rule 4 requires the recipient to reject a message that
has multiple
Content-Lengthfield-values that disagree (MUSTclosethe connection and respond
400 Bad Request).BaseHTTPRequestHandlercalls
http.client.parse_headersand never inspectsheaders.get_all("Content-Length"), so a request such asis dispatched to the handler. A handler that reads
int(self.headers.get("Content-Length"))silently sees only the firstvalue (
4), while a different handler that takesget_alland picksthe last value sees
0. The behaviour is implementation-dependenteven within the standard library, which is the exact situation RFC
7230 §3.3.3 was written to prevent.
Lib/http/server.pyBaseHTTPRequestHandler.parse_request400 Bad Request+Connection: closeDefect B —
Transfer-Encodingheader is accepted but never decodedRFC 7230 §3.3.3 rule 3 states that if a
Transfer-Encodingheader ispresent, it overrides
Content-Lengthand the recipient must dechunkthe body.
BaseHTTPRequestHandlerdoes not implement a chunkeddecoder, but it also does not reject the header, so a request such as
is accepted and a handler that reads
int(Content-Length)bytesleaves the rest of the chunked frame (and the trailing request) in the
socket buffer, so the keep-alive loop parses
GET /pwn HTTP/1.1as anew top-level request line. Per RFC 7230 §3.3.3 the correct response
is
400 Bad Request+ close, because the server cannot honour theTransfer-Encodingsemantics it received.Lib/http/server.pyBaseHTTPRequestHandler.parse_request400 Bad Request+Connection: closeDefect C — request body is not drained between requests on a persistent connection
RFC 7230 §6.3 requires the server to either consume the entire
request body or close the connection before reading the next request
on a persistent connection.
BaseHTTPRequestHandler.handle_one_requestdoes neither: after
do_<method>()returns, the next iteration ofhandle()callsrfile.readline(65537)directly. A request such asresults in
SimpleHTTPRequestHandler(which does not read the body ofa
GET) leaving 100 bytes plus the smuggled request line in thebuffer. The keep-alive loop then reads
AAAA...AAAAGET /pwn HTTP/1.1as a malformed request line and replies501 Unsupported method ('AAAA...GET'), generated entirely fromattacker-controlled input.
Lib/http/server.pyBaseHTTPRequestHandler.handle_one_request/handleReachability
http.serveris documented as not for production, butBaseHTTPRequestHandleris the basis forwsgiref.simple_server,which is used in many quick-start guides, development servers
(
manage.py runserverstyle flows, Flaskapp.runin someconfigurations, plenty of
python -m http.servertutorials), CIfixtures, internal admin tools and embedded devices. RFC 7230 framing
rules are the foundation of HTTP request-boundary integrity; honouring
them does not change semantics for any well-formed client and only
costs three header checks plus a bounded body drain.
Reproduction
Three self-contained reproductions (one per defect) are available on
request; each starts a
ThreadingHTTPServeron127.0.0.1, sends theraw bytes shown above over
socket.socket, and prints the observedresponse.
Suggested fix
A single PR can address all three defects in
BaseHTTPRequestHandler:parse_request, afterhttp.client.parse_headersreturns,reject requests with conflicting
Content-Lengthvalues (defect A).parse_request, after the same point, reject anyTransfer-Encodingheader (defect B) — oncehttp.servergrows achunked decoder this check can be narrowed to "anything other than
identity".handle_one_request, wrapself.rfilewith a smallbyte-counting reader for the duration of the request, then after
do_<method>()returns drain up to a bounded number of bytes ofany unread declared body (defect C). If the declared body exceeds
the drain cap, close the connection instead.
All three checks are conservative: well-formed clients are unaffected.
Linked PR
A pull request implementing the fix and adding regression tests in
Lib/test/test_httpservers.pyplus aMisc/NEWS.dentry is beingprepared.
Acknowledgement
Per PSRT (Stan Ulbrych, 2026-05-26 14:51 UTC and Seth Larson,
2026-05-26 21:55 UTC) these defects are out of scope for the security
process and were invited to be filed as a public issue plus PR.
Reported by
tonghuaroot.Linked PRs