diff --git a/binding/binding_test.go b/binding/binding_test.go index f90488cd..5d23e447 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -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"}`) diff --git a/binding/body.go b/binding/body.go new file mode 100644 index 00000000..9864d98c --- /dev/null +++ b/binding/body.go @@ -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 +} diff --git a/binding/bson.go b/binding/bson.go index 464890f0..12127ac6 100644 --- a/binding/bson.go +++ b/binding/bson.go @@ -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) } diff --git a/binding/protobuf.go b/binding/protobuf.go index 259ae8e7..c6493960 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -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 } diff --git a/docs/doc.md b/docs/doc.md index d1c33b87..2ac8d3f3 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -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"`.