middleware.BodyLimit: a single oversized Read can silently bypass the limit
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 74/100
Hướng nghiên cứu
Bắt đầu từ implementation của limitedReader được middleware.BodyLimit, entry point của middleware, sử dụng, sau đó tái hiện vấn đề bằng ví dụ chunked-request và một reader trả về nhiều hơn giới hạn. Thêm regression coverage cho các lần đọc streaming vượt quá giới hạn và xác minh rằng không có dữ liệu nào được cung cấp sau khi vượt giới hạn, đồng thời lỗi đã cấu hình vẫn có thể quan sát được.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Tested against Echo v4.15.2, reproduces on Go 1.26 and Go 1.27 alike.
limitedReader.Read forwards the caller's buffer to the underlying reader unbounded, and only checks the cumulative count afterward — critically, it's not sticky: once tripped, it keeps handing back more real data on every subsequent call:
func (r *limitedReader) Read(b []byte) (n int, err error) {
n, err = r.reader.Read(b)
r.read += int64(n)
if r.read > r.limit {
return n, echo.ErrStatusRequestEntityTooLarge
}
return
}
io.Reader's own documentation says:
Callers should always process the n > 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors.
Any consumer following that documented, sanctioned pattern — including encoding/json.Decoder, which scans for a complete value before checking the trailing read error — can read arbitrarily far past the configured limit, since limitedReader keeps handing back more real data on every subsequent call after the limit has already been crossed.
Minimal reproduction, custom reader
Demonstrates the core defect directly, independent of Go version, no JSON involved: https://go.dev/play/p/vJbhmQqvI0Y
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
const limit = 5 // bytes
body := bytes.Repeat([]byte("x"), 10*limit)
e := echo.New()
e.POST("/", func(c echo.Context) error {
buf := make([]byte, 64)
var total int
for {
n, err := c.Request().Body.Read(buf)
total += n
if n == 0 {
break
}
// Processing n>0 bytes before treating a non-nil err as fatal
// is exactly what io.Reader's docs say callers should do.
_ = err
}
fmt.Printf("total bytes read: %d (configured limit: %d)\n", total, limit)
return c.NoContent(http.StatusOK)
}, middleware.BodyLimit(fmt.Sprintf("%dB", limit)))
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
// Force the content-read path instead of the Content-Length fast path,
// as with a real request whose size isn't known up front (chunked).
req.ContentLength = -1
req.TransferEncoding = []string{"chunked"}
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
}
Output:
total bytes read: 50 (configured limit: 5)
Every byte of the oversized body was delivered to the caller despite the 5-byte limit.
Encoding/json angle, for context
This is not just contrived — encoding/json.Decoder follows exactly this documented pattern, so BodyLimit + c.Bind() can silently accept oversized JSON bodies too. This is most reliably triggered on Go 1.27, where encoding/json.Decoder is backed by encoding/json/v2 by default, whose buffer-growth curve happens to align the crossing-the-limit read with completion of the value for common payload sizes — but as the reproduction above shows, the underlying issue is not Go-version-specific.
Minimal reproduction, JSON: https://go.dev/play/p/x9suEwdIO2x
Suggested fix
Mirror net/http.MaxBytesReader. Two changes are both required — bounding the read alone is not enough, since a caller that keeps calling Read after getting an error alongside n > 0 can still assemble the full body one small chunk at a time:
-
Bound the request size. Never ask the underlying reader for more than
remaining+1bytes, so a singleReadcan never return enough data to both cross the limit and complete a value:remaining := r.limit - r.read if int64(len(b))-1 > remaining { b = b[:remaining+1] } -
Make it sticky. Once the limit is crossed, permanently return
(0, err)on every subsequent call — never hand back more real data, no matter how many more times the caller retries:func (r *limitedReader) Read(b []byte) (n int, err error) { if r.err != nil { return 0, r.err } ... // bound as above, then on overflow: r.err = echo.ErrStatusRequestEntityTooLarge }
Happy to submit a PR with this change if it's welcome.
- Ngôn ngữ chính
- Go
- Star
- 32.7k
- Fork
- 2.8k
- Merge trung bình
- 9 giờ 39 phút
- Pull request đã merge (30 ngày)
- 6
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của labstack/echo
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 65/100
-
format tag conflicts with swag Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Tất cả issue của labstack/echo
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
crossplane/crossplane#7859 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
bazel-contrib/rules_go#4721 · 2 bình luận ·
-
needs-triage
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 92/100
-
bug carvel-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
carvel-dev/kapp-controller#1861 ·
-
area/logging kind/bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100