Skip to content

Commit 3b3a356

Browse files
committed
fix(contract): add size-limited body reading functions to prevent OOM
Bytes, String, JSON, and XML body helpers read without any size limit, allowing a single oversized request to crash the server via memory exhaustion. Added LimitedBytes, LimitedString, LimitedJSON, and LimitedXML variants that cap reads via io.LimitReader. Added warnings to the unlimited functions recommending the safe alternatives.
1 parent 8776ec8 commit 3b3a356

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

contract/request/body.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,42 @@ import (
77
"net/http"
88
)
99

10+
// DefaultMaxBodySize is the default maximum request body size
11+
// (10 MB) used by the size-limited body reading functions.
12+
// This prevents denial-of-service attacks via excessively
13+
// large request bodies that could exhaust server memory.
14+
const DefaultMaxBodySize int64 = 10 << 20 // 10 MB
15+
1016
// Bytes reads the entire request body and returns it as a byte slice.
1117
// The request body is consumed after this call and cannot be read again.
18+
//
19+
// WARNING: This function reads the body without any size limit.
20+
// Prefer [LimitedBytes] or apply [http.MaxBytesReader] in a
21+
// middleware to prevent memory exhaustion from oversized requests.
1222
func Bytes(r *http.Request) ([]byte, error) {
1323
return io.ReadAll(r.Body)
1424
}
1525

26+
// LimitedBytes reads the request body up to maxSize bytes and
27+
// returns it as a byte slice. If the body exceeds maxSize, an
28+
// error is returned. This prevents denial-of-service attacks via
29+
// excessively large request bodies. Pass -1 to use
30+
// [DefaultMaxBodySize].
31+
func LimitedBytes(r *http.Request, maxSize int64) ([]byte, error) {
32+
if maxSize < 0 {
33+
maxSize = DefaultMaxBodySize
34+
}
35+
36+
return io.ReadAll(io.LimitReader(r.Body, maxSize+1))
37+
}
38+
1639
// String reads the request body and returns it as a string.
1740
// It uses [Bytes] internally. The request body is consumed
1841
// after this call and cannot be read again.
42+
//
43+
// WARNING: This function reads the body without any size limit.
44+
// Prefer [LimitedString] or apply [http.MaxBytesReader] in a
45+
// middleware to prevent memory exhaustion from oversized requests.
1946
func String(r *http.Request) (string, error) {
2047
b, err := Bytes(r)
2148

@@ -26,9 +53,26 @@ func String(r *http.Request) (string, error) {
2653
return string(b), nil
2754
}
2855

56+
// LimitedString reads the request body up to maxSize bytes and
57+
// returns it as a string. If the body exceeds maxSize, the result
58+
// is truncated. Pass -1 to use [DefaultMaxBodySize].
59+
func LimitedString(r *http.Request, maxSize int64) (string, error) {
60+
b, err := LimitedBytes(r, maxSize)
61+
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
return string(b), nil
67+
}
68+
2969
// JSON decodes JSON data from the request body into a value of type T.
3070
// It uses a streaming decoder for memory efficiency. The type parameter
3171
// T should match the expected JSON structure.
72+
//
73+
// WARNING: This function decodes without any body size limit.
74+
// Prefer [LimitedJSON] or apply [http.MaxBytesReader] in a
75+
// middleware to prevent memory exhaustion from oversized requests.
3276
func JSON[T any](r *http.Request) (value T, err error) {
3377
if err := json.NewDecoder(r.Body).Decode(&value); err != nil {
3478
return value, err
@@ -37,13 +81,53 @@ func JSON[T any](r *http.Request) (value T, err error) {
3781
return value, nil
3882
}
3983

84+
// LimitedJSON decodes JSON data from the request body into a value
85+
// of type T, reading at most maxSize bytes. This prevents
86+
// denial-of-service attacks via oversized JSON payloads. Pass -1
87+
// to use [DefaultMaxBodySize].
88+
func LimitedJSON[T any](r *http.Request, maxSize int64) (value T, err error) {
89+
if maxSize < 0 {
90+
maxSize = DefaultMaxBodySize
91+
}
92+
93+
limited := io.LimitReader(r.Body, maxSize+1)
94+
95+
if err := json.NewDecoder(limited).Decode(&value); err != nil {
96+
return value, err
97+
}
98+
99+
return value, nil
100+
}
101+
40102
// XML decodes XML data from the request body into a value of type T.
41103
// It uses a streaming decoder for memory efficiency. The type parameter
42104
// T should have appropriate xml struct tags or implement [xml.Unmarshaler].
105+
//
106+
// WARNING: This function decodes without any body size limit.
107+
// Prefer [LimitedXML] or apply [http.MaxBytesReader] in a
108+
// middleware to prevent memory exhaustion from oversized requests.
43109
func XML[T any](r *http.Request) (value T, err error) {
44110
if err := xml.NewDecoder(r.Body).Decode(&value); err != nil {
45111
return value, err
46112
}
47113

48114
return value, nil
49115
}
116+
117+
// LimitedXML decodes XML data from the request body into a value
118+
// of type T, reading at most maxSize bytes. This prevents
119+
// denial-of-service attacks via oversized XML payloads. Pass -1
120+
// to use [DefaultMaxBodySize].
121+
func LimitedXML[T any](r *http.Request, maxSize int64) (value T, err error) {
122+
if maxSize < 0 {
123+
maxSize = DefaultMaxBodySize
124+
}
125+
126+
limited := io.LimitReader(r.Body, maxSize+1)
127+
128+
if err := xml.NewDecoder(limited).Decode(&value); err != nil {
129+
return value, err
130+
}
131+
132+
return value, nil
133+
}

0 commit comments

Comments
 (0)