mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 22:53:34 +08:00
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:
parent
dcaa4296d1
commit
98bc3d1347
@ -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
31
binding/body.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user