python / python/cpython

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

オープン
#150,499 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

stdlib type-bug
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

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

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

Lib/http/server.py から始め、特に BaseHTTPRequestHandler.parse_request と handle_one_request/handle を確認し、その後 Lib/test/test_httpservers.py の関連する回帰テストのカバレッジを確認してください。issue にある 3 つの再現例を使って、400 を返して閉じる場合または body を処理する場合に期待される動作を理解し、リンクされた PRs と Misc/NEWS.d のエントリが完了した作業を反映していることを確認してください。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
backend, networking
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。