fix(binding): add optional size limit for BSON and Protobuf bodies

BSON and Protobuf binders used io.ReadAll with no cap. Add
binding.MaxBodyBytes (default 0, unlimited) and a shared reader so
apps can bound those binders without changing historical defaults.
Oversized bodies return *http.MaxBytesError, which MustBindWith
already maps to HTTP 413.

Fixes #4759

Signed-off-by: elix3r <157088510+22elix3r@users.noreply.github.com>
This commit is contained in:
elix3r 2026-08-27 07:30:16 +05:30
parent dcaa4296d1
commit 98bc3d1347
5 changed files with 125 additions and 4 deletions

View File

@ -747,6 +747,96 @@ func TestBindingBSON(t *testing.T) {
string(data[1:]))
}
func TestBindingBSONBodySizeLimit(t *testing.T) {
orig := MaxBodyBytes
MaxBodyBytes = 16
t.Cleanup(func() { MaxBodyBytes = orig })
var obj any
req := requestWithBody(http.MethodPost, "/", strings.Repeat("x", 32))
err := BSON.Bind(req, &obj)
require.Error(t, err)
var maxErr *http.MaxBytesError
require.ErrorAs(t, err, &maxErr)
assert.Equal(t, int64(16), maxErr.Limit)
}
func TestBindingProtoBufBodySizeLimit(t *testing.T) {
orig := MaxBodyBytes
MaxBodyBytes = 8
t.Cleanup(func() { MaxBodyBytes = orig })
obj := protoexample.Test{}
req := requestWithBody(http.MethodPost, "/", strings.Repeat("x", 32))
err := ProtoBuf.Bind(req, &obj)
require.Error(t, err)
var maxErr *http.MaxBytesError
require.ErrorAs(t, err, &maxErr)
assert.Equal(t, int64(8), maxErr.Limit)
}
func TestBindingProtoBufBodySizeLimitReadError(t *testing.T) {
orig := MaxBodyBytes
MaxBodyBytes = 32
t.Cleanup(func() { MaxBodyBytes = orig })
obj := protoexample.Test{}
req := requestWithBody(http.MethodPost, "/", "")
req.Body = io.NopCloser(&hook{})
require.Error(t, ProtoBuf.Bind(req, &obj))
}
func TestBindingProtoBufBodySizeLimitAllowsBodyWithinLimit(t *testing.T) {
test := &protoexample.Test{
Label: proto.String("yes"),
}
data, err := proto.Marshal(test)
require.NoError(t, err)
orig := MaxBodyBytes
MaxBodyBytes = int64(len(data))
t.Cleanup(func() { MaxBodyBytes = orig })
obj := protoexample.Test{}
req := requestWithBody(http.MethodPost, "/", string(data))
require.NoError(t, ProtoBuf.Bind(req, &obj))
assert.Equal(t, "yes", *obj.Label)
}
func TestBindingBodySizeLimitDisabledByDefault(t *testing.T) {
assert.Equal(t, int64(0), MaxBodyBytes)
orig := MaxBodyBytes
MaxBodyBytes = 0
t.Cleanup(func() { MaxBodyBytes = orig })
var obj FooStruct
obj.Foo = "bar"
data, err := bson.Marshal(&obj)
require.NoError(t, err)
var got FooStruct
req := requestWithBody(http.MethodPost, "/", string(data))
require.NoError(t, BSON.Bind(req, &got))
assert.Equal(t, "bar", got.Foo)
}
func TestBindingBodySizeLimitAllowsBodyWithinLimit(t *testing.T) {
var obj FooStruct
obj.Foo = "bar"
data, err := bson.Marshal(&obj)
require.NoError(t, err)
orig := MaxBodyBytes
MaxBodyBytes = int64(len(data))
t.Cleanup(func() { MaxBodyBytes = orig })
var got FooStruct
req := requestWithBody(http.MethodPost, "/", string(data))
require.NoError(t, BSON.Bind(req, &got))
assert.Equal(t, "bar", got.Foo)
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)

31
binding/body.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2026 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"io"
"net/http"
)
// MaxBodyBytes is the maximum number of bytes the BSON and Protobuf binders
// will read from a request body. Zero (the default) means no limit, matching
// historical behavior. Applications that bind untrusted BSON or Protobuf
// input should set this to a positive value during initialization, for
// example 32 << 20.
var MaxBodyBytes int64
func readBody(r io.Reader) ([]byte, error) {
if MaxBodyBytes <= 0 {
return io.ReadAll(r)
}
body, err := io.ReadAll(io.LimitReader(r, MaxBodyBytes+1))
if err != nil {
return nil, err
}
if int64(len(body)) > MaxBodyBytes {
return nil, &http.MaxBytesError{Limit: MaxBodyBytes}
}
return body, nil
}

View File

@ -5,7 +5,6 @@
package binding
import (
"io"
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
@ -18,7 +17,7 @@ func (bsonBinding) Name() string {
}
func (b bsonBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
buf, err := readBody(req.Body)
if err == nil {
err = b.BindBody(buf, obj)
}

View File

@ -6,7 +6,6 @@ package binding
import (
"errors"
"io"
"net/http"
"google.golang.org/protobuf/proto"
@ -19,7 +18,7 @@ func (protobufBinding) Name() string {
}
func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
buf, err := readBody(req.Body)
if err != nil {
return err
}

View File

@ -817,6 +817,8 @@ func main() {
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz).
JSON, XML, YAML, and TOML binders stream the request body. BSON and Protobuf binders buffer the full body first. By default that buffer is unbounded (historical behavior). To cap it, set `binding.MaxBodyBytes` at process start, for example `binding.MaxBodyBytes = 32 << 20`. Oversized bodies return `*http.MaxBytesError`, and `MustBindWith` responds with HTTP 413.
Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://pkg.go.dev/github.com/go-playground/validator#hdr-Baked_In_Validators_and_Tags).
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.