upload

package
v0.63.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package upload is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.

Index

Constants

View Source
const MaxFilenameBytes = 255

MaxFilenameBytes caps the sanitised filename length. A user-supplied filename has no legitimate reason to exceed a few hundred bytes; the cap protects log lines, filesystem APIs, and database columns from pathological inputs.

View Source
const SanitizeFilenameInputBound = 4 * MaxFilenameBytes

SanitizeFilenameInputBound caps the *input* length before any of the O(n) sanitisation passes (control-byte strip, replacer, interior-ext split-and-join) run. A multipart Content-Disposition filename is a MIME-header value and is NOT counted against ParseMultipartForm's maxMemory, so without this guard an attacker can ship a multi-MiB filename (bounded only by the stdlib's 10 MiB per-header cap) that amplifies into tens of MB of transient allocation and >100ms of CPU per request — strings.Split on a ~9 MiB all-dots name produces a ~9.4M-element slice. The bound is a generous multiple of MaxFilenameBytes so legitimate names with escaped / multibyte sequences survive untouched; the final MaxFilenameBytes cap still applies after sanitisation.

Variables

View Source
var ErrInvalidKey = errors.New("upload: invalid key")

ErrInvalidKey is wrapped when a storage key is rejected by sanitization (path traversal, empty key, or a path that escapes the base directory). The detection lives in [sanitizeKey] and the backend's escape check — callers (e.g. ServeHandler) classify the typed error rather than re-implement path validation.

View Source
var ErrNotFound = errNotFound{}

ErrNotFound is wrapped by Get when the requested key doesn't exist. Callers can match on this or on errors.Is(err, os.ErrNotExist) — the returned error wraps both so existing code continues to work.

Functions

func Handler

func Handler(cfg Config) http.HandlerFunc

Handler returns an http.HandlerFunc that processes multipart file uploads. It expects a single file in the "file" form field. On success it responds with 200 and JSON Metadata.

func SanitizeFilename

func SanitizeFilename(name string) string

SanitizeFilename removes path separators, null bytes, and other dangerous characters from a filename to prevent path traversal attacks. It also neutralises double-extension smuggling — e.g. `shell.php.jpg` becomes `shell_php.jpg` — so a misconfigured web server can't be tricked into executing a hidden interior extension.

Control bytes (CR, LF, TAB, anything < 0x20) are dropped so a logged filename can't escape its log line via injected newlines or terminal control sequences. The final result is truncated to MaxFilenameBytes (preserving the extension) so an attacker can't ship a 10 MB filename.

func ServeHandler added in v0.18.0

func ServeHandler(storage Storage) http.HandlerFunc

ServeHandler returns an http.HandlerFunc that streams a file stored under storage back to the client. Mount it on the framework router with a catch-all key, e.g.

r.Get("/uploads/{key...}", upload.ServeHandler(storage))

Semantics:

  • GET and HEAD only; any other method yields 405 with an Allow header.
  • The key is resolved from r.PathValue("key") (populated by the router's {key...} wildcard), falling back to the URL path with a leading slash stripped when no path value is set.
  • The content type is sniffed from the first 512 bytes of the body via http.DetectContentType — never from the client or the key.
  • X-Content-Type-Options: nosniff is always set.
  • Scriptable content (HTML, XHTML, SVG) is forced to application/octet-stream with Content-Disposition: attachment so a browser downloads it instead of rendering it (stored-XSS guard).
  • Path-traversal defense is delegated entirely to the Storage backend (LocalStorage's sanitizeKey is the single enforcement point); the handler performs no path manipulation of its own and echoes no filesystem path on any error.
  • When the backend implements RangeGetter, `Range:` requests are answered with 206 and Accept-Ranges is advertised, so an interrupted download of a large object resumes instead of restarting. Backends that decline the capability serve whole bodies exactly as before.

func ValidateExt

func ValidateExt(filename string, allowed []string) error

ValidateExt checks that the file extension is in the allowed list. Extensions are compared case-insensitively without the leading dot. If allowed is empty, all extensions are permitted.

func ValidateMIME

func ValidateMIME(file io.ReadSeeker, allowed []string) error

ValidateMIME reads the first 512 bytes from file to detect MIME type, checks it against the allowed list, then resets the reader. If allowed is empty, all MIME types are permitted.

func ValidateSize

func ValidateSize(size int64, max int64) error

ValidateSize checks that the file size does not exceed max. A max of 0 means no limit.

Types

type Config

type Config struct {
	MaxSize      int64    // Maximum file size in bytes (0 = no limit)
	AllowedTypes []string // MIME type whitelist (empty = allow all)
	AllowedExts  []string // Extension whitelist (empty = allow all)
	Storage      Storage  // Storage backend implementation
}

Config holds configuration for the upload handler.

type LocalStorage

type LocalStorage struct {
	// contains filtered or unexported fields
}

LocalStorage implements Storage using the local filesystem.

func NewLocalStorage

func NewLocalStorage(baseDir string) *LocalStorage

NewLocalStorage creates a LocalStorage that saves files under baseDir.

func (*LocalStorage) Delete

func (s *LocalStorage) Delete(_ context.Context, key string) error

Delete removes the file at key from the local filesystem.

func (*LocalStorage) Exists

func (s *LocalStorage) Exists(_ context.Context, key string) (bool, error)

Exists checks whether a file at key exists in the local filesystem.

func (*LocalStorage) Get

func (s *LocalStorage) Get(_ context.Context, key string) (io.ReadCloser, error)

Get opens the file at key from the local filesystem for reading.

Returns ErrNotFound (wrapping os.ErrNotExist) when the key is missing — callers can match on os.ErrNotExist or upload.ErrNotFound without parsing the message. Other errors are returned with the absolute filesystem path stripped, so a 500 propagated to an end user doesn't disclose where the data lives.

func (*LocalStorage) GetRange added in v0.46.0

func (s *LocalStorage) GetRange(_ context.Context, key string) (io.ReadSeekCloser, error)

GetRange implements RangeGetter. The local backend already opens an *os.File, so seekability costs nothing here — Get simply discarded it through the io.ReadCloser return type. Key validation is the same code path, not a parallel one.

func (*LocalStorage) Save

func (s *LocalStorage) Save(_ context.Context, key string, r io.Reader) error

Save writes the file to the local filesystem under baseDir/key. It creates subdirectories as needed.

type Metadata

type Metadata struct {
	OriginalName string    `json:"originalName"`
	Size         int64     `json:"size"`
	MimeType     string    `json:"mimeType"`
	UploadedAt   time.Time `json:"uploadedAt"`
	Key          string    `json:"key"`
}

Metadata holds information about an uploaded file.

type RangeGetter added in v0.46.0

type RangeGetter interface {
	GetRange(ctx context.Context, key string) (io.ReadSeekCloser, error)
}

RangeGetter is an optional capability a Storage backend may implement to expose seekable reads.

Storage.Get returns an io.ReadCloser, which erases seekability, and http.ServeContent needs an io.ReadSeeker to answer a `Range:` request. Without this, a client that loses its connection 1.8 GB into a 2 GB download restarts from zero, and browsers/CDNs that probe with a range request get a 200 with the whole body instead of a 206.

It is a capability interface rather than a widening of Storage on purpose: a network-backed store would have to buffer the whole object to satisfy Seek, which is worse than declining. Callers type-assert and fall back — ServeHandler is the reference consumer.

An implementation MUST apply the same key validation as its Get: a capability that skipped the traversal check would be a path-traversal hole with a performance justification.

type Storage

type Storage interface {
	Save(ctx context.Context, key string, r io.Reader) error
	Delete(ctx context.Context, key string) error
	Get(ctx context.Context, key string) (io.ReadCloser, error)
	Exists(ctx context.Context, key string) (bool, error)
}

Storage defines the interface for file storage backends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL