python / python/cpython

http.server: BaseHTTPRequestHandler does not enforce RFC 7230 framing validation (3 defects, hardening for keep-alive)

Open
#150,499 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stdlib type-bug
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:

  1. In parse_request, after http.client.parse_headers returns,
    reject requests with conflicting Content-Length values (defect A).
  2. 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".
  3. 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
  • gh-150500
  • gh-150508

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.