Skip to content

fix(middleware): prevent BodyLimit bypass with contract-compliant readers - #3087

Open
istoolsfox wants to merge 1 commit into
labstack:masterfrom
istoolsfox:fix/body-limit-bypass
Open

fix(middleware): prevent BodyLimit bypass with contract-compliant readers#3087
istoolsfox wants to merge 1 commit into
labstack:masterfrom
istoolsfox:fix/body-limit-bypass

Conversation

@istoolsfox

Copy link
Copy Markdown

Problem

limitedReader.Read forwards the caller's buffer to the underlying reader unbounded and only checks the cumulative count afterwards. Any consumer following the documented io.Reader pattern — process the n>0 bytes before considering the error, then call Read again — can drain an oversized body in full, because the reader keeps handing out real data on every subsequent call after the limit has been crossed.

encoding/json.Decoder follows exactly that documented pattern, so BodyLimit + c.Bind() can silently accept oversized JSON bodies too (most visible on Go 1.27 where json/v2 backs json.Decoder).

Reproduction from #3071: with a 5-byte limit and a 50-byte chunked body, a handler reading into a 64-byte buffer receives all 50 bytes.

Fix

Align limitedReader with net/http's maxBytesReader:

  • cap the read buffer to remaining + 1 bytes, so a single Read can never pull more than the limit allows (the one extra byte only tells us whether the underlying reader holds more data than allowed),
  • on the crossing read, hand out only the allowed portion and return 413 Request Entity Too Large,
  • make the error sticky: every subsequent Read returns 0 and the 413 error, never more data.

A body of exactly the limit size keeps working: the crossing check is n > remaining, so exact-size bodies still terminate with a clean EOF.

Tests

New cases in body_limit_test.go:

  • TestBodyLimitReader_singleOversizedRead — a single read with a buffer much larger than the limit delivers at most the allowed bytes and reports 413.
  • TestBodyLimitReader_noDataAfterLimitExceeded — a caller that keeps calling Read (processing n>0 first, per the io.Reader docs) receives no data after the limit is exceeded.
  • TestBodyLimitReader_exactLimitBody — a body of exactly the limit size is still read completely (regression guard).
  • TestBodyLimit_oversizedBodyWithContractCompliantReader — end-to-end middleware case from middleware.BodyLimit: a single oversized Read can silently bypass the limit #3071 (ContentLength = -1, chunked); on the previous code it reads 50 bytes under a 5-byte limit, with the fix it reads at most 5.

All existing BodyLimit tests pass unchanged; gofmt, go vet and the full test suite are clean.

Fixes #3071

…ders

limitedReader.Read forwarded the caller's buffer to the underlying
reader unbounded and only checked the cumulative count afterwards.
Per the io.Reader contract, callers process n>0 bytes before
considering the error and then call Read again - which handed out
more real data every time, so an oversized body could be drained in
full despite the configured limit (also triggered by
encoding/json.Decoder via c.Bind()).

Align the reader with net/http's maxBytesReader: cap the buffer one
byte past the limit, deliver only the allowed portion on the crossing
read, and keep the error sticky so no further data is handed out.
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.

middleware.BodyLimit: a single oversized Read can silently bypass the limit

1 participant