Skip to content

Commit fb34af0

Browse files
committed
fix(middleware): stop BodyLimit handing out more than the limit
limitedReader.Read passed the caller's buffer to the source untouched and only looked at the running total afterwards, and the refusal did not stick. io.Reader asks callers to process the n>0 bytes of a read before treating its error as fatal, so a caller following that advice — encoding/json's Decoder among them — kept getting real data on every call after the limit had already been passed. With a 5 byte limit and a 50 byte body, 50 bytes came through. The read is now capped at one byte past the limit, which is all it takes to know the body is too large; that byte is not handed to the caller; and once the limit is passed the reader stays refused without touching the source again. Fixes #3071
1 parent e552b4d commit fb34af0

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

middleware/body_limit.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,30 @@ func (config BodyLimitConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
8181
}
8282

8383
func (r *limitedReader) Read(b []byte) (n int, err error) {
84+
// Once the limit is known to be exceeded, stay refused. io.Reader's
85+
// contract invites callers to process the n>0 bytes of a failed read and
86+
// carry on, so a reader that keeps serving data after the first refusal
87+
// hands out the whole body to anyone following that advice.
88+
if r.read > r.LimitBytes {
89+
return 0, echo.ErrStatusRequestEntityTooLarge
90+
}
91+
92+
// Never read further than one byte past the limit: that byte is what
93+
// proves the body is too large, and anything beyond it is data the caller
94+
// asked for but is not allowed to have.
95+
if max := r.LimitBytes - r.read + 1; int64(len(b)) > max {
96+
b = b[:max]
97+
}
98+
8499
n, err = r.reader.Read(b)
85100
r.read += int64(n)
86101
if r.read > r.LimitBytes {
102+
// Hand back only what fits. The byte past the limit was read to prove
103+
// the body is too large, not to be delivered, and a caller that
104+
// processes n>0 before the error must not receive it.
105+
if over := int(r.read - r.LimitBytes); over <= n {
106+
n -= over
107+
}
87108
return n, echo.ErrStatusRequestEntityTooLarge
88109
}
89110
return

middleware/body_limit_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,89 @@ func TestBodyLimitAfterDecompressUsesDecodedSize(t *testing.T) {
9393
assert.Equal(t, body, rec.Body.String())
9494
}
9595

96+
func TestBodyLimitReaderStopsAtTheLimit(t *testing.T) {
97+
const limit = 5
98+
body := bytes.Repeat([]byte("x"), 10*limit)
99+
100+
reader := &limitedReader{
101+
BodyLimitConfig: BodyLimitConfig{Skipper: DefaultSkipper, LimitBytes: limit},
102+
reader: io.NopCloser(bytes.NewReader(body)),
103+
}
104+
105+
// io.Reader asks callers to process the n>0 bytes of a read before
106+
// treating its error as fatal, so keep reading the way such a caller
107+
// would. No more than the limit may be handed over however long it goes on.
108+
buf := make([]byte, 64)
109+
var total int
110+
for range 20 {
111+
n, err := reader.Read(buf)
112+
total += n
113+
if n == 0 && err != nil {
114+
break
115+
}
116+
}
117+
118+
assert.Equal(t, limit, total)
119+
}
120+
121+
// countingReader records how much was asked of the source, which is not
122+
// visible from what the caller receives.
123+
type countingReader struct {
124+
io.Reader
125+
read int64
126+
calls int
127+
}
128+
129+
func (c *countingReader) Read(b []byte) (int, error) {
130+
c.calls++
131+
n, err := c.Reader.Read(b)
132+
c.read += int64(n)
133+
return n, err
134+
}
135+
136+
func (c *countingReader) Close() error { return nil }
137+
138+
func TestBodyLimitReaderDoesNotOverdrawTheSource(t *testing.T) {
139+
const limit = 5
140+
src := &countingReader{Reader: bytes.NewReader(bytes.Repeat([]byte("x"), 1<<20))}
141+
142+
reader := &limitedReader{
143+
BodyLimitConfig: BodyLimitConfig{Skipper: DefaultSkipper, LimitBytes: limit},
144+
reader: src,
145+
}
146+
147+
buf := make([]byte, 64*1024)
148+
_, _ = reader.Read(buf)
149+
callsAtRefusal := src.calls
150+
151+
// One byte past the limit is enough to know the body is too large; a
152+
// megabyte of it should never be pulled off the wire to find that out.
153+
assert.LessOrEqual(t, src.read, int64(limit+1))
154+
155+
// And once refused, the source must not be touched again.
156+
for range 5 {
157+
_, _ = reader.Read(buf)
158+
}
159+
assert.Equal(t, callsAtRefusal, src.calls)
160+
}
161+
162+
func TestBodyLimitReaderStaysRefused(t *testing.T) {
163+
reader := &limitedReader{
164+
BodyLimitConfig: BodyLimitConfig{Skipper: DefaultSkipper, LimitBytes: 2},
165+
reader: io.NopCloser(bytes.NewReader([]byte("Hello, World!"))),
166+
}
167+
168+
_, err := io.ReadAll(reader)
169+
he := err.(echo.HTTPStatusCoder)
170+
assert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())
171+
172+
// Reading on after the refusal must not produce more of the body.
173+
n, err := reader.Read(make([]byte, 8))
174+
assert.Equal(t, 0, n)
175+
he = err.(echo.HTTPStatusCoder)
176+
assert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())
177+
}
178+
96179
func TestBodyLimitReader(t *testing.T) {
97180
hw := []byte("Hello, World!")
98181

0 commit comments

Comments
 (0)