KAHIBARO
Discord Login Register

File Upload Security

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:

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:

Typical scenario:

  1. App accepts files and writes them to /var/www/uploads/.
  2. Web server is configured to execute .php files anywhere under /var/www.
  3. Attacker uploads shell.php containing malicious PHP code.
  4. Attacker visits https://example.com/uploads/shell.php and 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:

txt
filename = "../../../../../etc/passwd"

or

txt
filename = "../config.py"

If your code does something like:

python
open("/var/www/uploads/" + filename, "wb")

this can overwrite or create files outside /var/www/uploads/.

Malware and Viruses

Users can upload:

Even if your server does not execute these files, you might:

Denial of Service (DoS) via Large Files

Huge uploads can:

Attack patterns:

Example of a “zip bomb”:

Side‑Channel Information Leaks

File uploads can be abused to:

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:

Designing Safe Upload Handling

Separate Concerns: Receive, Store, Serve, Process

Think of file handling as four separate steps:

  1. Receive the file
    Accept the raw bytes, enforce size limits and basic checks.
  2. Store the file
    Put it in safe storage, with secure paths and names.
  3. Serve the file (if needed)
    Return files to clients through a controlled mechanism.
  4. 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:

  1. Allow only whitelisted file types.
  2. Never trust the file name, extension, or MIME type from the client.
  3. Store uploads outside the web root whenever possible.
  4. Generate your own random file names.
  5. Enforce strict size limits on both request and file.
  6. Validate and sanitize paths, names, and content.
  7. Restrict who can upload and how often.
  8. 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:

Whitelist vs Blacklist

ApproachDescriptionProblem
BlacklistBlock some known bad typesAttackers use unknown or new types
WhitelistAllow only a small set of known good typesEverything else is rejected by default

Always prefer a whitelist. For example:

text
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:

So you must verify on the server side.

Check File Extension and MIME Type

You can:

Example in Python using python-magic:

python
import magic
def detect_mime(file_bytes: bytes) -> str:
    return magic.from_buffer(file_bytes, mime=True)

Basic workflow:

  1. Read a small chunk from the file.
  2. Detect MIME type with a library.
  3. 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):

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 False

You can also:

For example:

python
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_HEIGHT

File Size Limits and Quotas

Reasons to Limit Size

Without limits, attackers can:

Set limits at multiple levels:

  1. HTTP server or reverse proxy (Nginx, Traefik, etc.)
  2. Application server (Uvicorn / Gunicorn / frameworks)
  3. Application code (per file and per request)

Example Size Policy

TypeLimit example
Request body size20 MB total
Single file size5 MB
User total storage500 MB per user
Files per requestMax 5 files

Enforcing Size in Code

Example in pseudocode:

python
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 data

Do 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:

python
save_path = "/var/www/uploads/" + original_filename

Better:

python
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:

python
filename = safe_filename(file.filename)
save_path = UPLOAD_DIR / filename

Prevent Path Traversal

Even if you generate your own file name, still be careful when constructing paths.

Always:

Example check:

python
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 path

Remove or Normalize Metadata

Even if you store the original filename (for display to users), sanitize it:

Example:

python
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:

Example: Serving Files via Backend

Simple pattern:

python
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:

Be Careful with Content Type and Disposition

When serving files, set headers explicitly.

Key headers:

Security consideration:

Example:

http
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:

then you need extra protection.

Use Safe Libraries

Pick libraries that:

Examples of risky processing:

Isolate Risky Processing

For high risk operations, consider:

Pattern:

  1. User uploads a file.
  2. Your app stores the raw file.
  3. Your app enqueues a background job (for example in Redis + Celery).
  4. Worker container reads the file, processes it, writes result.
  5. Worker container has no access to critical services or secrets.

Limit Resource Usage

When processing files, always limit:

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:

Basic pattern:

  1. Receive and store the file in a quarantine area.
  2. Scan the file.
  3. If clean, move to the "safe" storage.
  4. If infected, delete and log the incident, and notify the user appropriately.

Pseudo code:

python
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_path

Scanning Tradeoffs

Authentication, Authorization, and Rate Limiting

Control Who Can Upload

Do not allow anonymous uploads unless absolutely required.

Typical policy:

Example:

RoleMax file sizeAllowed types
user5 MBimages only
staff20 MBimages, PDFs, docs
admin100 MBmore, but still limited

Ownership and Access Control

Attach metadata to each uploaded file:

Use this data when serving or listing files.

Rate Limiting

To prevent abuse, limit:

Patterns:

Example rate limit rule:

Handling Compressed and Archive Files

Archives like .zip, .tar, .rar are tricky.

Risks:

Safe Archive Extraction

If you must accept archives and extract them:

  1. Validate the archive type.
  2. Before extraction:
    • Inspect each entry name.
    • Reject entries with path traversal patterns like ../.
    • Reject absolute paths starting with /, C:\, etc.
  3. Enforce limits:
    • Max number of files.
    • Max total uncompressed size.
    • Max depth of directory nesting.

Example check in pseudocode:

python
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 True

Do all checks before extracting.


Secure Uploads in Cloud and Object Storage

Often you will store uploads in object stores, for example:

This changes where files live, but not the need for security.

Presigned URLs

A common pattern:

  1. Client requests permission to upload a file.
  2. Backend validates user and file metadata (size, type hint).
  3. Backend generates a short lived presigned URL for direct upload to S3.
  4. Client uploads directly to S3 using that URL.
  5. Backend gets a callback or the client notifies completion.

Security points:

Public vs Private Buckets

Proceed as:

Logging, Monitoring, and Error Handling

Log Upload Events

Record:

Be careful not to log the full file contents. That can leak sensitive data.

Monitor for Abnormal Patterns

Examples of suspicious behavior:

Set alerts for such patterns.

Safe Error Responses

When an upload fails:

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

  1. Authentication & Authorization
    • Only authenticated users can upload, unless truly public by design.
    • Enforce per user and per role limits.
  2. File Type Restrictions
    • Maintain a whitelist of allowed extensions and MIME types.
    • Inspect file content using a library, not only client headers.
  3. File Size Limits
    • Limit request body size at server and app level.
    • Limit per file size and per user total storage.
  4. 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.
  5. Serving Files
    • Serve through backend logic, not directly executable by the web server.
    • Use safe Content-Type and Content-Disposition headers.
    • Apply access control when serving.
  6. Processing and Scanning
    • Use safe, maintained libraries.
    • Sandbox resource intensive or risky processing.
    • Use antivirus scanning for shared or downloaded files if appropriate.
  7. Archives and Complex Formats
    • Limit extraction size and file count.
    • Block path traversal inside archives.
  8. Cloud Storage
    • Use presigned URLs with strict size and time limits.
    • Keep buckets private unless content is intended to be public.
  9. 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

Comments

Please login to add a comment.

Don't have an account? Register now!