http.server: BaseHTTPRequestHandler does not enforce RFC 7230 framing validation (3 defects, hardening for keep-alive)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 77.2k
- Forks
- 35.9k
- PR merge metrics
- PR metrics pending
Description
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, afterhttp.client.parse_headersreturns,
reject requests with conflictingContent-Lengthvalues (defect A). - In
parse_request, after the same point, reject any
Transfer-Encodingheader (defect B) — oncehttp.servergrows a
chunked decoder this check can be narrowed to "anything other than
identity". - In
handle_one_request, wrapself.rfilewith 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
- gh-150500
- gh-150508
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Lib/http/server.py, especially BaseHTTPRequestHandler.parse_request and handle_one_request/handle, then review the related regression coverage in Lib/test/test_httpservers.py. Use the three reproductions in the issue to understand the expected 400-and-close or body-handling behavior, and check that the linked PRs and Misc/NEWS.d entry reflect the completed work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100