# File upload endpoints have size and type restrictions

- **Pattern:** `ab-002197` (`saas-api-design.api-security.file-upload-restrictions`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002197
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002197)

## Why it matters

Unrestricted file uploads are the intersection of CWE-434 (unrestricted upload of dangerous file type) and CWE-770 (resource exhaustion). Accepting unbounded file sizes lets attackers fill your storage quota and exhaust memory parsing the upload body. Validating file type only by extension or the caller-controlled `Content-Type` header is trivially bypassed: renaming `exploit.php` to `exploit.png` and setting `Content-Type: image/png` defeats both checks. OWASP API 2023 flags this under API4 (unrestricted resource consumption) and API8 (security misconfiguration).

## Severity rationale

High because malicious file uploads can exhaust storage, escape to arbitrary code execution if served directly, and bypass type checks that rely on attacker-controlled metadata.

## Remediation

Harden every upload endpoint in `app/api/` with three independent controls. First, limit size at the parser level. Second, validate MIME type from actual file bytes using the `file-type` package — Content-Type headers are attacker-controlled and cannot be trusted. Third, enforce an allowlist:

```ts
import { fileTypeFromBuffer } from 'file-type'

const MAX_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf'])

const arrayBuffer = await file.arrayBuffer()
if (arrayBuffer.byteLength > MAX_SIZE) return apiError('FILE_TOO_LARGE', '...', 413)

const detected = await fileTypeFromBuffer(Buffer.from(arrayBuffer))
if (!detected || !ALLOWED_TYPES.has(detected.mime)) {
  return apiError('INVALID_FILE_TYPE', '...', 415)
}
```

Store the file in object storage (S3, Supabase Storage, R2) under a random UUID key — never the user-supplied filename.

## Detection

- **ID:** `file-upload-restrictions`
- **Severity:** `high`
- **What to look for:** Enumerate all API routes that handle file uploads (look for `multipart/form-data` handling, `FormData`, `multer`, `formidable`, `busboy`, direct file buffer handling, or Next.js `formData()` calls). For each upload endpoint, check: (1) Is there a maximum file size limit enforced? (2) Is there a file type allowlist (MIME type check and/or file extension check)? (3) Is the MIME type validated from the actual file content, not just the filename or Content-Type header (which can be spoofed)?
- **Pass criteria:** At least 100% of file upload endpoints enforce: an explicit maximum file size of no more than 10MB, a file type allowlist based on MIME type inspection (not just extension or Content-Type header alone), and rejection of disallowed types with a clear error response.
- **Fail criteria:** Any upload endpoint without a size limit; or type validation based only on file extension or Content-Type header (easily spoofed); or no type validation at all.
- **Skip (N/A) when:** No file upload functionality exists in the project. Signal: no `multipart/form-data` handling, no `multer`/`formidable`/`busboy` dependencies, no `FormData` processing in any route handler.
- **Detail on fail:** Example: `POST /api/upload has no file size limit`. Identify which upload routes lack restrictions (e.g., "POST /api/upload has no file size limit; type is validated only by extension, not MIME content"). Max 500 chars.
- **Remediation:** Harden every file upload endpoint in `app/api/` with three controls. First, enforce a maximum file size at the parser level (e.g., `multer({ limits: { fileSize: 5 * 1024 * 1024 } })` for 5MB). Second, validate the MIME type from the actual file content — use the `file-type` npm package which reads the file's magic bytes rather than trusting the declared Content-Type. Third, maintain an explicit allowlist of permitted types (e.g., `['image/jpeg', 'image/png', 'image/webp', 'application/pdf']`) and reject anything not on it. Store uploaded files in cloud object storage (S3, Supabase Storage, Cloudflare R2) rather than the local filesystem, and give them random names (not user-supplied names) to prevent path traversal.

## External references

- cwe CWE-434
- cwe CWE-770
- owasp A04

Taxons: injection-and-input-trust, cost-efficiency

HTML version: https://auditbuffet.com/patterns/ab-002197
