File Upload Security
Table of Contents
Why File Upload Security Matters
Accepting file uploads looks simple, but it opens one of the most dangerous doors into your backend. A single insecure upload can let an attacker:
- Run code on your server
- Read or overwrite sensitive files
- Bypass authentication or authorization
- Fill up your disk and crash your app
So file upload security is not a “nice to have”. It is a core part of backend security.
Rule: Treat every uploaded file as untrusted and potentially malicious, no matter who sent it or what it is supposed to be.
In this chapter you will learn common risks and concrete patterns to handle uploads safely, with practical examples you can adapt to any backend stack.
Common Risks of File Uploads
Uploaded files create several types of risks. Understanding them helps you design the right defenses.
Remote Code Execution (RCE)
If an attacker can upload a script and get your server to execute it, you lose full control of the machine.
Examples:
- Uploading
shell.phpto a directory that the PHP web server executes - Uploading a
.jspto a Java app that serves it as executable code - Uploading a
.pyfile to a misconfigured Python server that imports it dynamically
Typical scenario:
- App accepts files and writes them to
/var/www/uploads/. - Web server is configured to execute
.phpfiles anywhere under/var/www. - Attacker uploads
shell.phpcontaining malicious PHP code. - Attacker visits
https://example.com/uploads/shell.phpand executes their code.
Path Traversal and Overwrites
If you use user input in file paths without proper checks, an attacker can escape the intended directory, or overwrite important files.
For example:
filename = "../../../../../etc/passwd"or
filename = "../config.py"If your code does something like:
open("/var/www/uploads/" + filename, "wb")
this can overwrite or create files outside /var/www/uploads/.
Malware and Viruses
Users can upload:
- Trojan horse executables
- Office documents with macros
- PDFs with exploits
- ZIP files containing malware
Even if your server does not execute these files, you might:
- Store them and later distribute them to other users
- Process them with vulnerable libraries
- Scan them on infected client machines
Denial of Service (DoS) via Large Files
Huge uploads can:
- Fill disk space
- Exhaust memory
- Block worker threads or processes
- Slow down or crash the system
Attack patterns:
- Uploading many large files
- Uploading an extremely large file slowly (slowloris style)
- Uploading "zip bombs" that decompress to enormous sizes
Example of a “zip bomb”:
- A 42 KB zip file that decompresses to multiple gigabytes
Side‑Channel Information Leaks
File uploads can be abused to:
- Probe your filesystem layout
- Discover which file types trigger errors
- Infer internal configuration from error messages
Example: returning stack traces that show full paths when a file fails to be processed.
Server-Side Request Forgery (SSRF) via File Parsing
If your server processes uploaded files and fetches external resources (for example, an image library loading remote URLs embedded in images), attackers might:
- Force your server to call internal services (
http://127.0.0.1:8080/admin) - Access metadata services in the cloud
- Scan internal network ports
Designing Safe Upload Handling
Separate Concerns: Receive, Store, Serve, Process
Think of file handling as four separate steps:
- Receive the file
Accept the raw bytes, enforce size limits and basic checks. - Store the file
Put it in safe storage, with secure paths and names. - Serve the file (if needed)
Return files to clients through a controlled mechanism. - Process the file (if needed)
Examine or transform the contents, ideally in isolation.
Design your backend so that each of these steps has its own protections.
General Security Principles for File Uploads
You want multiple layers of defense.
Core principles for safe upload handling:
- Allow only whitelisted file types.
- Never trust the file name, extension, or MIME type from the client.
- Store uploads outside the web root whenever possible.
- Generate your own random file names.
- Enforce strict size limits on both request and file.
- Validate and sanitize paths, names, and content.
- Restrict who can upload and how often.
- Use scanning and sandboxing for risky file types.
We will now go through these in more detail with examples.
Whitelisting Allowed File Types
Decide explicitly which types of files your application really needs. Anything else is rejected.
Good practice:
- Profile image upload: accept only
image/jpeg,image/png, maybeimage/webp. - Document upload: accept only
application/pdf, maybe a few office formats.
Whitelist vs Blacklist
| Approach | Description | Problem |
|---|---|---|
| Blacklist | Block some known bad types | Attackers use unknown or new types |
| Whitelist | Allow only a small set of known good types | Everything else is rejected by default |
Always prefer a whitelist. For example:
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"}
ALLOWED_MIME_TYPES = {"image/jpeg", "image/png"}Validating File Type and Content
Do Not Trust Client Supplied Metadata
The client can lie about:
- File extension:
cat.jpgthat is actually a.phpscript - Content type header:
Content-Type: image/jpegfor anything
So you must verify on the server side.
Check File Extension and MIME Type
You can:
- Read the file extension from the original name, but do not rely solely on it.
- Read the
Content-Typeheader. - Use a server side library to inspect the file's "magic bytes" or header.
Example in Python using python-magic:
import magic
def detect_mime(file_bytes: bytes) -> str:
return magic.from_buffer(file_bytes, mime=True)Basic workflow:
- Read a small chunk from the file.
- Detect MIME type with a library.
- Compare detected type with your allowed list.
If detected_type is not in ALLOWED_MIME_TYPES, reject the upload.
Validate Image Files Safely
For images, extra checks help.
Example with Pillow (Python):
from PIL import Image
from io import BytesIO
def is_valid_image(data: bytes) -> bool:
try:
img = Image.open(BytesIO(data))
img.verify() # verifies structure
return True
except Exception:
return FalseYou can also:
- Check dimensions (width, height)
- Reject unexpectedly large images
For example:
MAX_WIDTH = 4000
MAX_HEIGHT = 4000
def check_image_size(data: bytes) -> bool:
img = Image.open(BytesIO(data))
width, height = img.size
return width <= MAX_WIDTH and height <= MAX_HEIGHTFile Size Limits and Quotas
Reasons to Limit Size
Without limits, attackers can:
- Exhaust your disk
- Increase memory usage
- Make your app slow or unresponsive
Set limits at multiple levels:
- HTTP server or reverse proxy (Nginx, Traefik, etc.)
- Application server (Uvicorn / Gunicorn / frameworks)
- Application code (per file and per request)
Example Size Policy
| Type | Limit example |
|---|---|
| Request body size | 20 MB total |
| Single file size | 5 MB |
| User total storage | 500 MB per user |
| Files per request | Max 5 files |
Enforcing Size in Code
Example in pseudocode:
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
def save_uploaded_file(file):
size = 0
chunks = []
for chunk in file.iter_chunks(4096):
size += len(chunk)
if size > MAX_FILE_SIZE:
raise ValueError("File too large")
chunks.append(chunk)
data = b"".join(chunks)
# store dataDo not read unlimited data into memory at once. Use streaming and stop when the limit is reached.
Safe File Names and Paths
Generate Your Own File Names
Never use the original filename directly.
Bad:
save_path = "/var/www/uploads/" + original_filenameBetter:
import uuid
from pathlib import Path
UPLOAD_DIR = Path("/var/data/uploads")
def safe_filename(original_name: str) -> str:
ext = Path(original_name).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("Invalid extension")
return f"{uuid.uuid4().hex}{ext}"Then:
filename = safe_filename(file.filename)
save_path = UPLOAD_DIR / filenamePrevent Path Traversal
Even if you generate your own file name, still be careful when constructing paths.
Always:
- Use library functions like
os.path.joinorpathlib.Path - Use a dedicated base directory for uploads, for example
/var/data/uploads - Ensure you never escape this directory
Example check:
UPLOAD_DIR = Path("/var/data/uploads").resolve()
def make_upload_path(filename: str) -> Path:
path = (UPLOAD_DIR / filename).resolve()
if not str(path).startswith(str(UPLOAD_DIR)):
raise ValueError("Invalid path")
return pathRemove or Normalize Metadata
Even if you store the original filename (for display to users), sanitize it:
- Remove path separators
/and\ - Remove control characters
- Limit length, for example to 255 characters
Example:
import re
def sanitize_display_name(name: str) -> str:
name = name.strip()
name = name.replace("\\", "_").replace("/", "_")
name = re.sub(r"[^a-zA-Z0-9_. -]", "_", name)
return name[:255]Store this separately from the actual internal filename.
Storing and Serving Files Securely
Store Outside Web Root
If your web server serves static files directly, never put untrusted uploads where it can execute scripts.
Better approach:
- Store files in a non-public directory, for example
/var/data/uploads. - Serve files through your application logic, which can:
- Check permissions
- Set safe headers
- Control caching
- Apply rate limiting
Example: Serving Files via Backend
Simple pattern:
def download_file(user, file_id):
# 1. Look up file metadata in DB
file = db.get_file(file_id)
# 2. Check authorization
if file.owner_id != user.id and not user.is_admin:
raise HTTPForbidden()
# 3. Stream file from disk
path = Path(file.storage_path)
return stream_file(path, content_type=file.mime_type, filename=file.display_name)This ensures:
- Only authorized users can download
- You do not expose the actual directory structure
- The web server does not decide how to serve the file directly
Be Careful with Content Type and Disposition
When serving files, set headers explicitly.
Key headers:
Content-Type: correct media typeContent-Disposition:inlineorattachment; filename="..."
Security consideration:
- For untrusted file types like HTML or SVG, use
Content-Disposition: attachmentso browsers download instead of rendering.
Example:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="report.pdf"This prevents some cross site scripting or HTML injection through uploaded content.
Processing Files Safely
If you process uploaded files, for example:
- Generate thumbnails
- Extract text from documents
- Transcode video or audio
- Parse user supplied data
then you need extra protection.
Use Safe Libraries
Pick libraries that:
- Are widely used and maintained
- Have security updates
- Avoid executing embedded scripts (for example, disable macros)
Examples of risky processing:
- Running
ffmpegorImageMagickwith untrusted parameters - Using a PDF library that executes JavaScript
- Using a DOCX library that evaluates macros
Isolate Risky Processing
For high risk operations, consider:
- Processing files in a separate process
- Running that process in a container or sandbox with:
- Limited permissions
- Read-only filesystem
- No network access
Pattern:
- User uploads a file.
- Your app stores the raw file.
- Your app enqueues a background job (for example in Redis + Celery).
- Worker container reads the file, processes it, writes result.
- Worker container has no access to critical services or secrets.
Limit Resource Usage
When processing files, always limit:
- CPU time
- Memory
- Disk usage
- Temporary files
Many tools have flags for this. For example, for ImageMagick, you can limit memory and resource use in its policies configuration.
Antivirus and Malware Scanning
For some apps, especially when files are shared among users, malware scanning is essential.
Integrating a Scanner
You can use:
- A local antivirus engine (for example, ClamAV)
- A commercial scanning API
Basic pattern:
- Receive and store the file in a quarantine area.
- Scan the file.
- If clean, move to the "safe" storage.
- If infected, delete and log the incident, and notify the user appropriately.
Pseudo code:
def handle_upload(file):
temp_path = save_temp(file)
result = scan_with_clamav(temp_path)
if not result.is_clean:
delete_file(temp_path)
log_malware(result)
raise HTTPBadRequest("File failed security scan")
final_path = move_to_storage(temp_path)
return final_pathScanning Tradeoffs
- Scanning costs time and CPU.
- You might want to scan asynchronously and mark files as "pending" until the scan finishes.
- For some applications, you can allow download only after scanning is finished.
Authentication, Authorization, and Rate Limiting
Control Who Can Upload
Do not allow anonymous uploads unless absolutely required.
Typical policy:
- Only authenticated users can upload.
- Different roles have different quotas or types allowed.
Example:
| Role | Max file size | Allowed types |
|---|---|---|
| user | 5 MB | images only |
| staff | 20 MB | images, PDFs, docs |
| admin | 100 MB | more, but still limited |
Ownership and Access Control
Attach metadata to each uploaded file:
owner_idcreated_atvisibility(private, shared, public)allowed_roles
Use this data when serving or listing files.
Rate Limiting
To prevent abuse, limit:
- Uploads per minute per IP or user
- Total bandwidth used
Patterns:
- Use Redis based rate limiting for upload endpoints.
- Increase limits for trusted roles.
Example rate limit rule:
- Max 20 uploads per minute per user.
- Max 100 MB uploaded per hour per user.
Handling Compressed and Archive Files
Archives like .zip, .tar, .rar are tricky.
Risks:
- Zip bombs that decompress to huge sizes.
- Path traversal inside archives, for example entries with
../that could escape the target folder when extracted.
Safe Archive Extraction
If you must accept archives and extract them:
- Validate the archive type.
- Before extraction:
- Inspect each entry name.
- Reject entries with path traversal patterns like
../. - Reject absolute paths starting with
/,C:\, etc. - Enforce limits:
- Max number of files.
- Max total uncompressed size.
- Max depth of directory nesting.
Example check in pseudocode:
def is_safe_member(member_name: str) -> bool:
normalized = os.path.normpath(member_name)
if normalized.startswith("..") or normalized.startswith("/"):
return False
# additional checks
return TrueDo all checks before extracting.
Secure Uploads in Cloud and Object Storage
Often you will store uploads in object stores, for example:
- AWS S3
- MinIO (S3 compatible)
- GCS (Google Cloud Storage)
This changes where files live, but not the need for security.
Presigned URLs
A common pattern:
- Client requests permission to upload a file.
- Backend validates user and file metadata (size, type hint).
- Backend generates a short lived presigned URL for direct upload to S3.
- Client uploads directly to S3 using that URL.
- Backend gets a callback or the client notifies completion.
Security points:
- Presigned URLs should be short lived, for example 5 minutes.
- Limit the maximum object size in the presigned policy.
- Use unique object keys, generated server side.
- Store metadata in your database and never trust client supplied paths.
Public vs Private Buckets
- Private bucket, access only through your backend, is safest.
- Public bucket, or public prefix, can be allowed for non sensitive assets, but be very careful.
Proceed as:
- Use distinct buckets or prefixes for different content types and trust levels.
- Apply bucket policies that prevent public listing when not needed.
Logging, Monitoring, and Error Handling
Log Upload Events
Record:
- Who uploaded (user ID, IP)
- What they uploaded (file type, size)
- Where it was stored
- Whether scanning or validation failed
Be careful not to log the full file contents. That can leak sensitive data.
Monitor for Abnormal Patterns
Examples of suspicious behavior:
- Many failed upload attempts
- Uploads with unusual file types
- Sudden spike in upload sizes or frequency
- Antivirus detections
Set alerts for such patterns.
Safe Error Responses
When an upload fails:
- Do not include internal paths in error messages.
- Do not show stack traces to the user.
- Use generic messages like "Invalid file type" or "Upload failed, please try again".
Log full technical details only on the server side, for debugging.
Practical Secure Upload Checklist
Use this as a quick reference when you build upload endpoints.
Secure File Upload Checklist
- Authentication & Authorization
- Only authenticated users can upload, unless truly public by design.
- Enforce per user and per role limits.
- File Type Restrictions
- Maintain a whitelist of allowed extensions and MIME types.
- Inspect file content using a library, not only client headers.
- File Size Limits
- Limit request body size at server and app level.
- Limit per file size and per user total storage.
- File Names and Paths
- Generate random internal filenames.
- Store files in a dedicated directory, outside web root.
- Normalize and validate paths so they cannot escape the upload directory.
- Serving Files
- Serve through backend logic, not directly executable by the web server.
- Use safe
Content-TypeandContent-Dispositionheaders. - Apply access control when serving.
- Processing and Scanning
- Use safe, maintained libraries.
- Sandbox resource intensive or risky processing.
- Use antivirus scanning for shared or downloaded files if appropriate.
- Archives and Complex Formats
- Limit extraction size and file count.
- Block path traversal inside archives.
- Cloud Storage
- Use presigned URLs with strict size and time limits.
- Keep buckets private unless content is intended to be public.
- Observability
- Log uploads and validation results.
- Monitor for anomalies and malware detections.
- Return safe, non verbose error messages to clients.
With these patterns and checks in place, your backend can accept user files while keeping your system and your users much safer.
Views: 6
KAHIBARO