---
# === IDENTITY ===
id: software/security/file-upload-security/2026
canonical_question: "How do I implement secure file uploads?"
aliases:
  - "file upload security best practices"
  - "secure file upload implementation"
  - "prevent unrestricted file upload"
  - "CWE-434 prevention"
  - "file upload validation and sanitization"
  - "protect against malicious file uploads"
  - "file upload MIME type validation magic bytes"
  - "how to safely handle user file uploads"
entity_type: software_reference
domain: software > security > File Upload Security
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.95
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "OWASP File Upload Cheat Sheet updated 2024; CWE-434 remains top-25 weakness (2024 CWE Top 25)"
  next_review: 2026-08-26
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "NEVER trust the Content-Type header or file extension alone -- both are trivially spoofed by attackers"
  - "NEVER store uploaded files inside the web root or any directory with execute permissions"
  - "ALWAYS validate file content using magic bytes (file signature) in addition to extension allowlists"
  - "ALWAYS generate random filenames (UUID) server-side -- never use the original user-supplied filename for storage"
  - "ALWAYS enforce server-side file size limits -- client-side limits are trivially bypassed"
  - "NEVER process uploaded archives (ZIP, TAR) without decompression ratio limits to prevent zip bombs"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to prevent XSS via uploaded SVG or HTML files"
    use_instead: "software/security/xss-prevention/2026"
  - condition: "Need general input validation (not file-specific)"
    use_instead: "software/patterns/input-validation/2026"
  - condition: "Need cloud storage security (S3, GCS, Azure Blob)"
    use_instead: "software/cloud/object-storage-security/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "framework"
    question: "What web framework or language are you using?"
    type: choice
    options: ["Django/Python", "Express/Node.js", "Go", "Spring/Java", "Rails", "ASP.NET", "Other"]
  - key: "file_types"
    question: "What types of files do users need to upload?"
    type: choice
    options: ["Images only", "Documents (PDF, DOCX)", "Archives (ZIP, TAR)", "Mixed/any type", "Video/audio"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/security/file-upload-security/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention"
    - id: "software/patterns/input-validation/2026"
      label: "Input Validation Patterns"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/security/path-traversal-prevention/2026"
      label: "Path Traversal Prevention"
    - id: "software/security/csrf-prevention/2026"
      label: "CSRF Prevention"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "File Upload Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src2
    title: "CWE-434: Unrestricted Upload of File with Dangerous Type"
    author: MITRE
    url: https://cwe.mitre.org/data/definitions/434.html
    type: official_docs
    published: 2024-12-01
    reliability: authoritative
  - id: src3
    title: "Unrestricted File Upload"
    author: OWASP Foundation
    url: https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src4
    title: "File-Type Validation in Multer is NOT SAFE"
    author: DEV Community
    url: https://dev.to/ayanabilothman/file-type-validation-in-multer-is-not-safe-3h8l
    type: technical_blog
    published: 2024-09-15
    reliability: moderate_high
  - id: src5
    title: "Mastering File Upload Security: DoS Attacks and Antivirus"
    author: Theodo
    url: https://blog.theodo.com/2024/03/mastering-file-upload-security-dos-attacks-and-antivirus/
    type: technical_blog
    published: 2024-03-01
    reliability: moderate_high
  - id: src6
    title: "What is path traversal, and how to prevent it?"
    author: PortSwigger
    url: https://portswigger.net/web-security/file-path-traversal
    type: technical_blog
    published: 2025-01-10
    reliability: high
  - id: src7
    title: "mimetype: A Go package for media type detection based on magic numbers"
    author: Gabriel Vasile
    url: https://github.com/gabriel-vasile/mimetype
    type: official_docs
    published: 2025-06-01
    reliability: high
---

# File Upload Security: Preventing Unrestricted File Upload Attacks

## TL;DR

- **Bottom line**: Defense-in-depth with extension allowlisting, magic byte validation, random filename generation, and storage outside the web root prevents CWE-434 (unrestricted file upload) -- no single check is sufficient.
- **Key tool/command**: `python-magic` (Python), `file-type` npm package (Node.js), `http.DetectContentType()` (Go) -- validate actual file content, not just headers or extensions.
- **Watch out for**: Trusting the `Content-Type` header or file extension alone -- both are trivially spoofed; combine with magic byte inspection for real content validation.
- **Works with**: All web frameworks. OWASP File Upload Cheat Sheet applies universally. Framework-specific: Django `FileExtensionValidator` + `python-magic`, Express/Multer + `file-type`, Go `net/http.DetectContentType`.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER trust the Content-Type header or file extension alone -- both are trivially spoofed by attackers
- NEVER store uploaded files inside the web root or any directory with execute permissions
- ALWAYS validate file content using magic bytes (file signature) in addition to extension allowlists
- ALWAYS generate random filenames (UUID) server-side -- never use the original user-supplied filename for storage
- ALWAYS enforce server-side file size limits -- client-side limits are trivially bypassed
- NEVER process uploaded archives (ZIP, TAR) without decompression ratio limits to prevent zip bombs

## Quick Reference

| # | Vulnerability | Risk | Vulnerable Code | Secure Code |
|---|---|---|---|---|
| 1 | Unrestricted upload (CWE-434) | Critical -- RCE via web shell | No extension/type check | Allowlist extensions + magic byte validation |
| 2 | MIME type spoofing | High -- bypass content filters | `if (file.mimetype === 'image/png')` | Read first 512+ bytes, validate magic signature |
| 3 | Path traversal via filename | High -- arbitrary file write | `path.join(uploadDir, file.originalname)` | Generate UUID filename, strip all path components |
| 4 | Stored XSS via SVG/HTML upload | High -- XSS on same origin | Allow SVG/HTML uploads, serve inline | Serve with `Content-Disposition: attachment` + CSP |
| 5 | Malware upload | High -- infect other users | No scanning | Scan with ClamAV or cloud AV API before storage |
| 6 | Zip bomb / decompression bomb | Medium -- DoS, disk exhaustion | Extract without size checks | Enforce compression ratio < 100:1, max extracted size |
| 7 | Image bomb (pixel flood) | Medium -- DoS, memory exhaustion | Process image without dimension check | Enforce max pixel dimensions (e.g., 10000x10000) |
| 8 | Double extension bypass | High -- execute disguised file | Block `.php` but allow `file.php.jpg` | Check final extension only after stripping all dots |
| 9 | Null byte injection | High -- truncate filename | `file.php%00.jpg` passes extension check | Reject filenames containing null bytes or non-printable chars |
| 10 | Oversized upload DoS | Medium -- disk/bandwidth exhaustion | No file size limit | Enforce max size at reverse proxy AND application level |

## Decision Tree

```
START: What type of file upload does your application accept?
|-- Images only (JPEG, PNG, GIF, WebP)?
|   |-- YES -> Allowlist image extensions + validate magic bytes + re-encode image (strip metadata)
|   +-- NO v
|-- Documents (PDF, DOCX, XLSX)?
|   |-- YES -> Allowlist doc extensions + magic bytes + Content Disarm & Reconstruct (CDR) or AV scan
|   +-- NO v
|-- Archives (ZIP, TAR, GZ)?
|   |-- YES -> Allowlist archive extensions + magic bytes + enforce decompression ratio < 100:1 + max extracted size
|   +-- NO v
|-- Executable or script files needed?
|   |-- YES -> STOP: reconsider architecture. If unavoidable: sandbox + AV scan + never serve from web root
|   +-- NO v
|-- Mixed/unknown file types?
|   |-- YES -> Allowlist only required extensions + magic bytes + AV scan + serve with Content-Disposition: attachment
|   +-- NO v
+-- DEFAULT -> Allowlist extensions + magic byte validation + UUID filename + store outside web root
```

## Step-by-Step Guide

### 1. Configure server-side file size limits

Set maximum upload size at both the reverse proxy (Nginx/Apache) and application level. This is your first line of defense against DoS via oversized uploads. [src1]

```nginx
# Nginx: limit upload size to 10MB
client_max_body_size 10m;
```

```python
# Django settings.py
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024  # 10MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024
```

```javascript
// Express/Multer: limit to 10MB
const upload = multer({
  limits: { fileSize: 10 * 1024 * 1024 }  // 10MB
});
```

**Verify**: Upload a file larger than the limit -- should receive HTTP 413 (Request Entity Too Large) or framework-specific rejection.

### 2. Implement extension allowlisting

Only permit file extensions that your application specifically needs. Use an allowlist, never a blocklist. [src1]

```python
# Python/Django
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf'}

def validate_extension(filename):
    ext = os.path.splitext(filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        raise ValidationError(f'File type {ext} not allowed')
```

```javascript
// Node.js
const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf']);
const ext = path.extname(file.originalname).toLowerCase();
if (!ALLOWED_EXTENSIONS.has(ext)) {
  throw new Error(`File type ${ext} not allowed`);
}
```

**Verify**: Attempt to upload a `.php`, `.exe`, or `.html` file -- should be rejected.

### 3. Validate file content with magic bytes

The Content-Type header and file extension are user-controlled and trivially spoofed. Read the actual file bytes to verify content type. [src2]

```python
# Python: pip install python-magic
import magic

def validate_magic_bytes(file_path, allowed_mimes):
    """Validate file content matches allowed MIME types via magic bytes."""
    mime = magic.from_file(file_path, mime=True)
    if mime not in allowed_mimes:
        raise ValidationError(f'File content type {mime} not allowed')

ALLOWED_MIMES = {'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'}
validate_magic_bytes(uploaded_file_path, ALLOWED_MIMES)
```

```javascript
// Node.js: npm install file-type@19
import { fileTypeFromBuffer } from 'file-type';  // v19.x, ESM

const buffer = await fs.promises.readFile(tempFilePath);
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIMES.has(type.mime)) {
  throw new Error(`File content type ${type?.mime} not allowed`);
}
```

**Verify**: Rename a `.php` file to `.jpg` and upload -- magic byte check should reject it despite the valid extension.

### 4. Generate random filenames and store outside web root

Never use the user-supplied filename for storage. Generate a UUID and store files in a directory that is not web-accessible. [src1]

```python
import uuid
import os

UPLOAD_DIR = '/var/uploads'  # Outside web root, no execute permission

def store_file(uploaded_file, validated_ext):
    """Store file with random name outside web root."""
    safe_name = f'{uuid.uuid4().hex}{validated_ext}'
    dest_path = os.path.join(UPLOAD_DIR, safe_name)
    with open(dest_path, 'wb') as f:
        for chunk in uploaded_file.chunks():
            f.write(chunk)
    return safe_name
```

**Verify**: Check that stored files have UUID names and reside outside the web root directory. Verify the upload directory has `chmod 0640` (no execute).

### 5. Serve uploaded files safely

Never serve uploaded files directly from the filesystem. Use a controller that sets security headers and prevents direct execution. [src3]

```python
# Django: serve files through a view, not static serving
from django.http import FileResponse

def serve_upload(request, file_id):
    upload = get_object_or_404(Upload, id=file_id, user=request.user)
    response = FileResponse(open(upload.file_path, 'rb'))
    response['Content-Type'] = upload.validated_mime
    response['Content-Disposition'] = f'attachment; filename="{upload.original_name}"'
    response['X-Content-Type-Options'] = 'nosniff'
    response['Content-Security-Policy'] = "default-src 'none'"
    return response
```

**Verify**: Access an uploaded file URL directly -- should download (not render inline) with correct security headers.

### 6. Scan for malware (production environments)

For applications that accept file uploads from untrusted users, integrate virus scanning before the file is persisted or served to other users. [src5]

```bash
# ClamAV scanning via command line
clamscan --max-filesize=10M --max-scansize=100M uploaded_file.pdf

# Or via clamd socket (faster for production)
clamdscan uploaded_file.pdf
```

```python
# Python: pip install pyclamd
import pyclamd

def scan_file(file_path):
    """Scan uploaded file for malware. Returns True if clean."""
    cd = pyclamd.ClamdUnixSocket()
    result = cd.scan_file(file_path)
    if result is not None:
        raise ValidationError(f'Malware detected: {result}')
    return True
```

**Verify**: Upload an EICAR test file (standard AV test pattern) -- should be detected and rejected.

## Code Examples

### Python/Django: Complete Secure Upload Handler

> Full script: [python-django-complete-secure-upload-handler.py](scripts/python-django-complete-secure-upload-handler.py) (38 lines)

```python
# views.py -- secure file upload with all validation layers
import os, uuid, magic
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
# ... (see full script)
```

### Node.js/Express: Multer with Magic Byte Validation

> Full script: [node-js-express-multer-with-magic-byte-validation.js](scripts/node-js-express-multer-with-magic-byte-validation.js) (41 lines)

```javascript
// upload.js -- Express + Multer with magic byte validation
import express from 'express';                    // ^4.21.0
import multer from 'multer';                      // ^1.4.5
import { fileTypeFromBuffer } from 'file-type';   // ^19.0.0
import { randomUUID } from 'crypto';
# ... (see full script)
```

### Go: Secure File Upload Handler

> Full script: [go-secure-file-upload-handler.go](scripts/go-secure-file-upload-handler.go) (67 lines)

```go
package main
// Input:  multipart/form-data POST with "file" field
// Output: JSON { "id": "uuid.ext" } or error
import (
    "crypto/rand"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Trusting file extension only

```python
# BAD -- extension is user-controlled and trivially spoofed
def upload(request):
    file = request.FILES['file']
    if file.name.endswith('.jpg'):  # Attacker renames malware.php to malware.jpg
        file.save('/var/www/uploads/' + file.name)  # Stored in web root!
```

### Correct: Extension allowlist + magic bytes + UUID filename

```python
# GOOD -- multi-layer validation with safe storage
def upload(request):
    file = request.FILES['file']
    ext = os.path.splitext(file.name)[1].lower()
    if ext not in ALLOWED_EXT:
        raise ValidationError('Extension not allowed')
    mime = magic.from_buffer(file.read(2048), mime=True)
    file.seek(0)
    if mime not in ALLOWED_MIMES:
        raise ValidationError('Content type mismatch')
    safe_name = f'{uuid.uuid4().hex}{ext}'
    dest = os.path.join('/var/uploads', safe_name)  # Outside web root
    save_to_disk(file, dest)
```

### Wrong: Storing uploads in the web root

```javascript
// BAD -- files in web root can be executed by the web server
const upload = multer({ dest: 'public/uploads/' });
// Attacker uploads shell.php -> accessible at https://example.com/uploads/shell.php
```

### Correct: Store outside web root, serve via controller

```javascript
// GOOD -- files stored outside web root, served through authenticated route
const upload = multer({ dest: '/var/uploads/' });  // Not in public/

app.get('/files/:id', authenticate, (req, res) => {
  const filePath = path.join('/var/uploads', req.params.id);
  res.set('Content-Disposition', 'attachment');
  res.set('X-Content-Type-Options', 'nosniff');
  res.sendFile(filePath);
});
```

### Wrong: No file size limits

```javascript
// BAD -- no size limit allows DoS via multi-GB uploads
const upload = multer({ dest: '/tmp/uploads' });
app.post('/upload', upload.single('file'), handler);
```

### Correct: Enforce size limits at multiple layers

```javascript
// GOOD -- size limit at Multer + Nginx/reverse proxy
const upload = multer({
  dest: '/tmp/uploads',
  limits: { fileSize: 10 * 1024 * 1024 }  // 10MB
});
// Also set in Nginx: client_max_body_size 10m;
```

### Wrong: Using user-supplied filename for storage

```go
// BAD -- path traversal: filename could be "../../etc/passwd"
destPath := filepath.Join(uploadDir, header.Filename)
dst, _ := os.Create(destPath)
```

### Correct: Generate random filename, ignore user input

```go
// GOOD -- UUID filename prevents path traversal and collisions
randBytes := make([]byte, 16)
rand.Read(randBytes)
safeName := hex.EncodeToString(randBytes) + validatedExt
destPath := filepath.Join(uploadDir, safeName)
dst, _ := os.Create(destPath)
```

## Common Pitfalls

- **MIME spoofing via Content-Type header**: Multer's `file.mimetype` comes from the client-sent `Content-Type` header, which attackers control. Fix: Use `file-type` or `python-magic` to read actual file bytes. [src4]
- **Double extension bypass**: `malware.php.jpg` may pass extension checks but execute as PHP on misconfigured servers. Fix: Check only the final extension; configure web server to not execute files in upload directories. [src1]
- **Null byte injection**: `malware.php%00.jpg` truncates at the null byte on some systems. Fix: Reject filenames containing `%00`, null bytes, or non-printable characters. [src2]
- **SVG/HTML files enabling XSS**: SVG files can contain `<script>` tags; HTML files execute JavaScript if served inline from the same origin. Fix: Never serve SVG/HTML uploads with `Content-Type: text/html`; use `Content-Disposition: attachment`. [src3]
- **Zip bomb (decompression bomb)**: A 42KB ZIP can expand to 4.5PB. Fix: Enforce decompression ratio limit (< 100:1), maximum extracted file count, and maximum total extracted size. [src5]
- **Image bomb (pixel flood)**: A small file with declared dimensions of 100000x100000 pixels causes OOM when processed. Fix: Read image headers (dimensions) before processing; enforce max 10000x10000 pixels. [src5]
- **Path traversal via filename**: Filename `../../etc/cron.d/backdoor` writes outside intended directory. Fix: Generate UUID filenames; never use user input in file paths. Use `os.path.basename()` or equivalent if original name must be preserved for metadata. [src6]
- **Race condition between validation and storage**: File is validated, then replaced before it is moved to permanent storage (TOCTOU). Fix: Validate on the already-saved temp file; use atomic move operations. [src1]

## Diagnostic Commands

```bash
# Check file's real MIME type via magic bytes (Linux/macOS)
file --mime-type uploaded_file.jpg

# Check file magic signature (first 16 bytes as hex)
xxd -l 16 uploaded_file.jpg

# Known magic bytes: JPEG=FF D8 FF, PNG=89 50 4E 47, PDF=%PDF, ZIP=50 4B 03 04, GIF=47 49 46 38

# Scan file for malware with ClamAV
clamscan uploaded_file.pdf

# Check upload directory permissions (should NOT have execute)
ls -la /var/uploads/
# Expected: drw-r----- (no x bit)

# Check Nginx upload size limit
nginx -T 2>/dev/null | grep client_max_body_size

# Generate EICAR test file (standard AV test -- not actual malware)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > eicar.txt

# Find files with double extensions in upload directory
find /var/uploads -name '*.*.*' -type f

# Check for executable files in upload directory
find /var/uploads -type f -executable
```

## Version History & Compatibility

| Standard/Tool | Version | Status | Key Feature |
|---|---|---|---|
| OWASP File Upload Cheat Sheet | 2024 | Current | Comprehensive upload security guidance |
| CWE-434 | 4.19 | Current | Top-25 CWE weakness classification |
| python-magic | 0.4.x | Current | libmagic bindings for Python |
| file-type (npm) | 19.x | Current | Magic byte detection (ESM only) |
| file-type (npm) | 16.x | Maintained | CommonJS support (legacy) |
| multer (npm) | 1.4.x | Current | Express multipart middleware |
| Go net/http | 1.22+ | Current | `DetectContentType()` built-in (512 bytes) |
| gabriel-vasile/mimetype | 1.4.x | Current | Extended magic byte detection for Go |
| ClamAV | 1.3.x | Current | Open-source antivirus scanning |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Application accepts file uploads from any user | Application only stores files generated server-side | Standard filesystem security |
| Uploaded files are served back to other users | Files are only processed server-side and never served | Simpler validation may suffice |
| Upload includes images, documents, or archives | API only accepts JSON/text payloads (no binary files) | Input validation patterns |
| Running a multi-tenant platform with untrusted users | Single-user admin tool with trusted operators | Reduced validation + logging |
| Files are processed (image resize, document parse) | Files are stored as opaque blobs and never opened | Hash verification + size limits only |

## Important Caveats

- `http.DetectContentType()` in Go only checks the first 512 bytes and supports a limited set of MIME types -- for broader detection use `gabriel-vasile/mimetype` or `h2non/filetype`
- `python-magic` requires `libmagic` system library (`apt install libmagic1` on Debian/Ubuntu, `brew install libmagic` on macOS) -- without it, the library fails silently or crashes
- Multer's `fileFilter` runs before the file is fully written to disk -- magic byte validation must happen after the upload completes on the temp file
- ClamAV scanning adds 100-500ms per file; for high-throughput systems, use `clamd` daemon mode with Unix socket, not the `clamscan` CLI
- Image re-encoding (to strip metadata/payloads) may alter image quality -- always use lossless re-encoding for PNG/GIF and configurable quality for JPEG
- Cloud storage (S3, GCS) with signed URLs shifts some security to the storage layer, but client-side validation and magic byte checks are still required before upload

## Related Units
<!-- Generated from related_kos frontmatter -->

- [XSS Prevention](/software/security/xss-prevention/2026)
- [Input Validation Patterns](/software/patterns/input-validation/2026)
- [Path Traversal Prevention](/software/security/path-traversal-prevention/2026)
- [CSRF Prevention](/software/security/csrf-prevention/2026)
