Skip to content

gh-150499: http.server: enforce RFC 7230 framing rules for keep-alive connections#150500

Open
tonghuaroot wants to merge 1 commit into
python:mainfrom
tonghuaroot:gh-150499-httpserver-framing-validation
Open

gh-150499: http.server: enforce RFC 7230 framing rules for keep-alive connections#150500
tonghuaroot wants to merge 1 commit into
python:mainfrom
tonghuaroot:gh-150499-httpserver-framing-validation

Conversation

@tonghuaroot
Copy link
Copy Markdown

@tonghuaroot tonghuaroot commented May 27, 2026

Closes #150499.

Summary

BaseHTTPRequestHandler in Lib/http/server.py does not enforce three
RFC 7230 framing-validation rules, so a non-conforming request can
desynchronise the keep-alive loop. The issue collects the three
defects and the reproductions; this PR fixes all of them in a single
class:

  1. RFC 7230 §3.3.3 rule 4 — duplicate Content-Length headers
    with disagreeing values are now rejected with 400 Bad Request and
    Connection: close. Identical values are still accepted.
  2. RFC 7230 §3.3.3 rule 3 — any Transfer-Encoding header is now
    rejected with 400 Bad Request and Connection: close, because
    http.server does not implement a chunked decoder. Once a decoder
    is added this check can be narrowed to anything other than
    identity.
  3. RFC 7230 §6.3handle_one_request now wraps self.rfile
    with a small byte-counting reader for the duration of the request,
    and after the handler returns it drains any unread declared body up
    to a 1 MiB cap. If the remaining body exceeds that cap the
    connection is closed instead. This prevents the next iteration of
    the keep-alive loop from parsing leftover body bytes as a request
    line.

The wrapper proxies read, read1, readline, readinto, and
readinto1 and falls back to __getattr__ for everything else, so
existing handlers (including the ServerHandler chain in
wsgiref.simple_server, which passes self.rfile directly to WSGI
applications) continue to work unchanged.

Backwards compatibility

The module's class docstring already warns that http.server is not
intended for production use. The three checks tighten conformance to
RFC 7230 and only change behaviour for non-conforming clients:

  • Duplicate-equal Content-Length still works.
  • Transfer-Encoding was previously accepted and silently ignored,
    which is a request-smuggling primitive; clients sending TE must now
    either dechunk themselves or stop sending it.
  • The body-drain path is invisible to handlers that already read the
    full Content-Length body, and adds a bounded read for handlers that
    do not (such as SimpleHTTPRequestHandler's do_GET against a
    request with a body).

Tests

Lib/test/test_httpservers.py adds RFC7230FramingTestCase with:

  • test_duplicate_content_length_rejected — defect A.
  • test_duplicate_content_length_same_value_accepted — confirms the
    same-value case is unchanged.
  • test_transfer_encoding_rejected — defect B, plus assertion that
    the smuggled trailing request is not dispatched.
  • test_body_drained_on_persistent_connection — defect C, asserts
    that the leftover body is not parsed as the next request line.
  • test_keep_alive_post_pipeline — regression test for two pipelined
    POSTs with correct Content-Length on the same keep-alive
    connection.

All five tests run against a single-threaded HTTPServer (matching
the existing BaseTestCase setup) and pass in ~6 seconds locally on
Python 3.14.4 with the patched module.

News entry

Misc/NEWS.d/next/Library/2026-05-27-00-43-54.gh-issue-150499.ykruLi.rst.

Backport

This PR targets main only; per CPython practice the backport policy
is left to the core dev review.

Add three framing checks to BaseHTTPRequestHandler so the handler does
not desynchronise on a persistent connection:

* RFC 7230 section 3.3.3 rule 4: reject duplicate Content-Length
  headers whose values disagree, with 400 Bad Request and close.
* RFC 7230 section 3.3.3 rule 3: reject any Transfer-Encoding header
  with 400 Bad Request and close, because http.server does not
  implement a chunked decoder. Once a decoder is added this check can
  be narrowed to anything other than 'identity'.
* RFC 7230 section 6.3: wrap rfile with a small byte-counting reader
  for the duration of the request and, after the handler returns,
  drain any unread declared body up to a 1 MiB cap (or close the
  connection if the remainder is larger).

Add regression tests in Lib/test/test_httpservers.py covering the three
defects plus a pipelined keep-alive POST regression test that exercises
the new drain path.

Signed-off-by: tonghuaroot <tonghuaroot@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant