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