fix(body-limit): refuse chunked & malformed Content-Length (F3, F21)#11
Merged
Merged
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BodySizeLimitMiddlewareonly enforced the cap when aContent-Lengthheader was present and parsed as a non-negative int. Two bypasses:Transfer-Encoding: chunkedand noContent-Lengthflowed 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.try: int(raw); except: declared = 0. A hostile client sendingContent-Length: abcgot 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
Content-Length: 100, body 100 B, cap 1 KiBContent-Length: 9999, cap 1 KiBContent-Length: abc, body anythingContent-Length: -1, body anythingTransfer-Encoding: chunkedPOSTTest plan
test_malformed_content_length_header_treated_as_zero(which asserted the old silent-pass behaviour) → nowtest_malformed_content_length_header_returns_400test_negative_content_length_returns_400Related
Findings F3 (chunked bypass), F21 (malformed CL silently zero-ed) from internal security review punch list.
🤖 Generated with Claude Code