Skip to content

fix(body-limit): refuse chunked & malformed Content-Length (F3, F21)#11

Merged
jieyao-MilestoneHub merged 1 commit into
mainfrom
fix/body-limit-completeness
May 3, 2026
Merged

fix(body-limit): refuse chunked & malformed Content-Length (F3, F21)#11
jieyao-MilestoneHub merged 1 commit into
mainfrom
fix/body-limit-completeness

Conversation

@jieyao-MilestoneHub
Copy link
Copy Markdown
Contributor

Summary

BodySizeLimitMiddleware only enforced the cap when a Content-Length header was present and parsed as a non-negative int. Two bypasses:

  • F3 — chunked transfer-encoding bypass. A POST with Transfer-Encoding: chunked and no Content-Length flowed straight through. Starlette buffers the entire chunked body into memory before pydantic sees it, so the cap was ineffective for any client that chose chunked. Honest JSON clients (httpx, openai-python, langchain-openai, curl) always set Content-Length on JSON POSTs, so the practical impact of refusing chunked is zero. Now: body-bearing methods (POST / PUT / PATCH) without Content-Length are refused with 411 Length Required. Operators who genuinely need chunked uploads must front the gateway with a proxy that materialises Content-Length, or extend this middleware to a streaming ASGI implementation.
  • F21 — malformed Content-Length silently zero-ed. Old code did try: int(raw); except: declared = 0. A hostile client sending Content-Length: abc got a free pass — the body was unbounded, the cap-comparison saw 0. Now: malformed values return 400 Bad Request. Negative values also return 400.

Module docstring updated to reflect the stricter contract.

Behaviour change

Request shape Before After
Content-Length: 100, body 100 B, cap 1 KiB 200 ✓ 200 ✓
Content-Length: 9999, cap 1 KiB 413 ✓ 413 ✓
Content-Length: abc, body anything silently passes 400
Content-Length: -1, body anything silently passes 400
Transfer-Encoding: chunked POST silently passes 411
GET /anything, no Content-Length passes ✓ passes ✓
GET /health, no Content-Length passes ✓ passes ✓

Test plan

  • Updated existing test_malformed_content_length_header_treated_as_zero (which asserted the old silent-pass behaviour) → now test_malformed_content_length_header_returns_400
  • New test_negative_content_length_returns_400
  • CI: existing POST happy-path / GET-bypass / oversized tests continue to pass
  • CI: ruff + black + pyright pass

Related

Findings F3 (chunked bypass), F21 (malformed CL silently zero-ed) from internal security review punch list.

🤖 Generated with Claude Code

`BodySizeLimitMiddleware` only enforced the cap when a `Content-Length`
header was present and parsed as a non-negative int. Two bypasses:

* **F3 — chunked transfer-encoding bypass.** A POST with
  `Transfer-Encoding: chunked` and no `Content-Length` flowed straight
  through. Starlette buffers the entire chunked body into memory before
  pydantic sees it, so the cap was ineffective for any client that
  chose chunked. Honest JSON clients (httpx, openai-python,
  langchain-openai, curl) always set Content-Length on JSON POSTs, so
  the practical impact of refusing chunked is zero. Now: body-bearing
  methods (POST / PUT / PATCH) without Content-Length are refused with
  411 Length Required.

* **F21 — malformed Content-Length silently zero-ed.** The old code:
  `try: declared = int(raw)` `except ValueError: declared = 0`. A
  hostile client sending `Content-Length: abc` got a free pass — the
  body was unbounded, the cap-comparison saw 0. Now: malformed values
  return 400 Bad Request. Negative values also return 400.

Updates the module docstring to reflect the stricter contract and
adds the new RFC 9110 §9.3 method whitelist as a frozenset constant.

Tests:
- The existing `test_malformed_content_length_header_treated_as_zero`
  asserted the OLD silent-pass behaviour; renamed and inverted to
  `test_malformed_content_length_header_returns_400`.
- New `test_negative_content_length_returns_400` covers the
  negative-length boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jieyao-MilestoneHub jieyao-MilestoneHub merged commit 08a5405 into main May 3, 2026
2 checks passed
@jieyao-MilestoneHub jieyao-MilestoneHub deleted the fix/body-limit-completeness branch May 3, 2026 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant