-
Notifications
You must be signed in to change notification settings - Fork 150
Expand file tree
/
Copy pathform.go
More file actions
307 lines (277 loc) · 11.1 KB
/
form.go
File metadata and controls
307 lines (277 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package runtime
import (
stderrors "errors"
"fmt"
"mime/multipart"
"net/http"
"github.com/go-openapi/errors"
)
// DefaultMaxUploadFilenameLength is the default cap applied to
// FileHeader.Filename for each declared file when [BindForm] is invoked
// without an explicit [BindFormMaxFilenameLen] option.
//
// Multipart headers are allocated per part; an attacker submitting
// multi-MB filenames inflates the parser's memory footprint. 1 KiB
// matches the IETF guidance for sane filename length and is enough
// for realistic uploads.
const DefaultMaxUploadFilenameLength = 1024
// DefaultMaxUploadBodySize limits the size of the body to upload forms to 32MB.
//
// Use an explicit [BindFormMaxBody] option to change this limit.
const DefaultMaxUploadBodySize = int64(32) << 20
// filenamePreviewLen caps the byte length of the FileHeader.Filename
// preview embedded as the ParseError.Value field when the helper
// rejects a too-long filename.
const filenamePreviewLen = 32
// ValidateFilenameLength enforces the FileHeader.Filename length cap
// that [BindForm] applies via [BindFormFile] declarations. Untyped
// binder paths that fetch the file via [http.Request.FormFile]
// directly (rather than declaring the file through [BindFormFile]) call
// this to opt into the same protection.
//
// Returns nil if filename length is within maxLen or maxLen <= 0.
// Otherwise returns a [*errors.ParseError] suitable for direct return
// from a parameter binder. The error embeds a truncated preview of
// the offending filename to keep the error message bounded.
func ValidateFilenameLength(paramName, paramIn, filename string, maxLen int) error {
if maxLen <= 0 || len(filename) <= maxLen {
return nil
}
preview := filename[:min(len(filename), filenamePreviewLen)]
return errors.NewParseError(paramName, paramIn, preview,
fmt.Errorf("filename length %d exceeds limit %d", len(filename), maxLen))
}
// FileBinder is the per-file callback invoked by [BindForm] when a
// declared file field is present.
//
// The callback is responsible for BOTH validating the file (size, MIME, etc.) AND assigning the bound
// file to its destination — typically using:
//
// o.FieldName = &runtime.File{Data: file, Header: header}
//
// Returning a non-nil error surfaces the error in [BindForm]'s per-field
// accumulator. Errors from the binder flow through verbatim — the
// binder is expected to produce HTTP-aware errors (e.g.
// [errors.ExceedsMaximum] from go-openapi/validate).
type FileBinder func(file multipart.File, header *multipart.FileHeader) error
// BindOption configures [BindForm]. The variadic style keeps simple
// call sites simple and lets new knobs (security caps, additional
// behaviour) be added without breaking the signature.
type BindOption func(*bindConfig)
type bindConfig struct {
maxParseMemory int64
maxBody int64
maxFiles int
maxFilenameLen int
files []formFileSpec
}
type formFileSpec struct {
name string
required bool
bind FileBinder
}
// BindFormMaxParseMemory caps the in-memory portion of a multipart
// body. Bytes beyond this are spilled to temporary files on disk by
// the stdlib parser. 0 (the default) defers to the stdlib's 32 MB.
//
// This option does NOT cap total body bytes — see [BindFormMaxBody]
// for that. The default body cap ([DefaultMaxUploadBodySize] = 32 MB)
// is applied even when this option is not supplied, so out of the box
// [BindForm] is bounded; callers with stricter or looser requirements
// adjust via [BindFormMaxBody].
func BindFormMaxParseMemory(n int64) BindOption {
return func(c *bindConfig) { c.maxParseMemory = n }
}
// BindFormMaxBody caps the size of the body read from a http form before parsing.
//
// The limit is set to 32MB by default. This default limit is applied for any n=0.
//
// The limit is disabled for n<0, assuming the caller has already capped the body size upstream.
func BindFormMaxBody(n int64) BindOption {
return func(c *bindConfig) { c.maxBody = n }
}
// BindFormMaxFiles rejects parses where the total number of file
// parts across all field names exceeds n. 0 (the default) means no
// cap. Exceeding the cap is a fatal error — [BindForm] returns
// fatal=true and no per-file binders run.
func BindFormMaxFiles(n int) BindOption {
return func(c *bindConfig) { c.maxFiles = n }
}
// BindFormMaxFilenameLen rejects per-file headers whose Filename
// length exceeds n. 0 means no cap; the default applied when this
// option is not supplied is [DefaultMaxUploadFilenameLength]. The
// cap is a per-field bind error (non-fatal); other declared files
// still run.
func BindFormMaxFilenameLen(n int) BindOption {
return func(c *bindConfig) { c.maxFilenameLen = n }
}
// BindFormFile declares a file field to bind under the given form
// name. If required is true and the field is absent, [BindForm]
// produces the per-field error.
//
// errors.NewParseError(name, "formData", "", http.ErrMissingFile)
//
// If required is false, absence is silent (no error, no bind).
//
// The bind callback runs only when the field is present. It is the
// site where both validation and assignment happen — see [FileBinder].
//
// FileHeader.Filename is attacker-controlled text; the binder MUST
// NOT use it directly as a filesystem path. The helper does not
// touch the filesystem.
func BindFormFile(name string, required bool, bind FileBinder) BindOption {
return func(c *bindConfig) {
c.files = append(c.files, formFileSpec{
name: name,
required: required,
bind: bind,
})
}
}
// BindForm parses r as multipart/form-data, falling back to
// application/x-www-form-urlencoded when the request is not
// multipart. On success, r.MultipartForm and r.PostForm are populated;
// the caller can read non-file form values via [Values](r.Form) after
// the call returns.
//
// All errors produced by BindForm itself (parse failure, missing
// required field, cap exceeded) are [*errors.ParseError] values built
// via [errors.NewParseError], matching the untyped
// middleware/parameter.go path. Errors returned by per-file binders
// flow through verbatim — binders own their HTTP-aware error shape.
//
// Per-file binders declared via [BindFormFile] run in declaration
// order after a successful parse. Their errors are accumulated and
// returned wrapped in [errors.CompositeValidationError]; the caller
// typically appends the returned err to its own []error and continues
// with non-file parameter binding.
//
// Return semantics:
//
// - fatal=true, err!=nil: parse failure or a hard cap (e.g.
// [BindFormMaxFiles]) was exceeded. No per-file binders ran; the
// caller MUST return err immediately.
// - fatal=false, err!=nil: one or more per-file binders produced
// errors. The form parsed successfully; r.Form is populated. The
// caller appends err to its accumulator and continues.
// - fatal=false, err==nil: full success.
//
// fatal==true implies err!=nil.
//
// Defaults applied out of the box:
//
// - Total body bytes capped at [DefaultMaxUploadBodySize] (32 MB)
// via [http.MaxBytesReader]. Adjust with [BindFormMaxBody]
// (negative n disables, when the caller has already capped the
// body upstream).
// - FileHeader.Filename length capped at
// [DefaultMaxUploadFilenameLength]. Adjust with
// [BindFormMaxFilenameLen].
//
// Caller responsibilities the helper does NOT cover:
//
// - Set [http.Server.ReadTimeout] / [http.Server.IdleTimeout] to defend
// against slow-read attacks.
// - Decompress Content-Encoding: gzip request bodies upstream if
// the API accepts them, using a size-limited reader.
// - Treat FileHeader.Filename as untrusted user input; never use
// it directly as a filesystem path.
func BindForm(r *http.Request, opts ...BindOption) (fatal bool, err error) {
cfg := bindConfig{
maxFilenameLen: DefaultMaxUploadFilenameLength,
}
for _, opt := range opts {
opt(&cfg)
}
if perr := parseFormBody(r, cfg.maxParseMemory, cfg.maxBody); perr != nil {
// Body-cap hit gets the 413 status; everything else maps to a
// 400 ParseError. parseFormBody returns the raw stdlib error
// in both cases — the HTTP-aware wrapping happens here.
var maxBytesErr *http.MaxBytesError
if stderrors.As(perr, &maxBytesErr) {
return true, errors.New(http.StatusRequestEntityTooLarge, "formData: %v", perr)
}
return true, errors.NewParseError("body", "formData", "", perr)
}
if cfg.maxFiles > 0 {
if got := countFileParts(r); got > cfg.maxFiles {
return true, errors.NewParseError("body", "formData", "",
fmt.Errorf("multipart form contains %d file parts, exceeds limit %d", got, cfg.maxFiles))
}
}
var bindErrs []error
for _, spec := range cfg.files {
if e := bindFormFile(r, spec, cfg.maxFilenameLen); e != nil {
bindErrs = append(bindErrs, e)
}
}
if len(bindErrs) > 0 {
return false, errors.CompositeValidationError(bindErrs...)
}
return false, nil
}
// parseFormBody parses the request body. Content-Type drives the
// parser: multipart/form-data → r.ParseMultipartForm, everything else
// → r.ParseForm (stdlib's parsePostForm only actually reads the body
// when Content-Type is application/x-www-form-urlencoded, so calling
// ParseForm is safe for unrecognised types).
//
// Caveat: ParseMultipartForm calls ParseForm internally and discards its error
// when the body turns out not to be multipart, returning ErrNotMultipart instead
// — the subsequent retry then short-circuits because r.PostForm is already
// set. Content-type-based routing avoids the lossy detour.
//
// Returns the raw stdlib error on failure; the caller (BindForm)
// handles HTTP-aware wrapping (413 for MaxBytesError, 400 ParseError
// otherwise).
//
// maxMemory == 0 falls through to the stdlib default (32 MB).
// maxBody == 0 defaults to DefaultMaxUploadBodySize; maxBody < 0
// disables the body cap (caller has capped upstream).
func parseFormBody(r *http.Request, maxMemory, maxBody int64) error {
if r.Body != nil && maxBody >= 0 {
if maxBody == 0 {
maxBody = DefaultMaxUploadBodySize
}
r.Body = http.MaxBytesReader(nil, r.Body, maxBody)
}
mt, _, _ := ContentType(r.Header)
if mt == MultipartFormMime {
//nolint:gosec // G120: false positive -- see below
// gosec doesn't track the Body.
// See https://github.com/securego/gosec/blob/de65614d10a6b84029e3e1215567b8ce7e490f23/testutils/g120_samples.go#L57
return r.ParseMultipartForm(maxMemory)
}
return r.ParseForm()
}
func countFileParts(r *http.Request) int {
if r.MultipartForm == nil {
return 0
}
var n int
for _, fhs := range r.MultipartForm.File {
n += len(fhs)
}
return n
}
func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error {
file, header, err := r.FormFile(spec.name)
if err != nil {
if stderrors.Is(err, http.ErrMissingFile) {
if spec.required {
return errors.New(http.StatusBadRequest, "formData: %v", http.ErrMissingFile)
}
return nil
}
return errors.NewParseError(spec.name, "formData", "", err)
}
if err := ValidateFilenameLength(spec.name, "formData", header.Filename, maxFilenameLen); err != nil {
return err
}
if spec.bind == nil {
return nil
}
return spec.bind(file, header)
}