mirror of
https://github.com/moby/moby.git
synced 2026-08-08 17:11:38 +00:00
356 lines
13 KiB
Go
356 lines
13 KiB
Go
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package runtime
|
|
|
|
import (
|
|
stderrors "errors"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"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
|
|
}
|
|
|
|
// FormFile resolves a file field from a parsed form body, transparently
|
|
// handling both content types accepted for `type: file` parameters by
|
|
// the OpenAPI 2.0 spec:
|
|
//
|
|
// - multipart/form-data — delegates to [http.Request.FormFile].
|
|
// - application/x-www-form-urlencoded — looks up the field in
|
|
// r.PostForm and synthesizes a [multipart.File] backed by the
|
|
// value bytes plus a [multipart.FileHeader] with Filename equal
|
|
// to the field name and Size set to the byte length.
|
|
//
|
|
// Returns [http.ErrMissingFile] when the field is absent under either
|
|
// content type. Callers must have parsed the body upstream (e.g. via
|
|
// [BindForm] or [http.Request.ParseForm]) before reading from the
|
|
// urlencoded path — [http.Request.FormFile] takes care of parsing on
|
|
// the multipart path.
|
|
//
|
|
// Presence is the only criterion for binding a urlencoded file: an
|
|
// empty value (e.g. `file=`) is bound as a zero-byte file.
|
|
func FormFile(r *http.Request, name string) (multipart.File, *multipart.FileHeader, error) {
|
|
file, header, err := r.FormFile(name)
|
|
if err == nil {
|
|
return file, header, nil
|
|
}
|
|
if !stderrors.Is(err, http.ErrNotMultipart) {
|
|
return nil, nil, err
|
|
}
|
|
|
|
values, present := r.PostForm[name]
|
|
if !present {
|
|
return nil, nil, http.ErrMissingFile
|
|
}
|
|
value := values[0]
|
|
return urlencodedFile{Reader: strings.NewReader(value)},
|
|
&multipart.FileHeader{Filename: name, Size: int64(len(value))},
|
|
nil
|
|
}
|
|
|
|
// urlencodedFile adapts a urlencoded form value (already buffered in
|
|
// memory by [http.Request.ParseForm]) to the [multipart.File]
|
|
// interface. The embedded [strings.Reader] supplies Read/ReadAt/Seek;
|
|
// Close is a no-op since there is no resource to release.
|
|
type urlencodedFile struct {
|
|
*strings.Reader
|
|
}
|
|
|
|
func (urlencodedFile) Close() error { return nil }
|
|
|
|
func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error {
|
|
file, header, err := FormFile(r, 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)
|
|
}
|