KAHIBARO
Discord Login Register

5.9.8. Secure File Handling

Why Secure File Handling Matters

Letting users upload or download files is powerful, but it is also one of the most dangerous features in a backend.

Attackers can try to:

Never treat user-provided files as safe. Always validate, restrict, and isolate them.

In this chapter we focus on practical rules, patterns, and examples that you can apply independently of any specific framework.


General Principles for Secure File Handling

Treat all user input as untrusted

A file is just another kind of user input. Everything about it is untrusted:

You must not:

Instead, you inspect and restrict every important aspect of the file.


Defense in depth

You do not rely on a single check. You combine many small protections.

Typical layers:

LayerExample
Access controlOnly logged in users may upload files.
Size limitsReject files over 5 MB.
Type validationOnly allow images jpg, png, gif.
Storage isolationStore uploads outside public web root.
Name randomizationUse generated IDs instead of original file names.
Virus scanningScan files with an antivirus before accepting.
Authorization on accessOnly owner or admin can download a file.

Secure file handling is a combination of multiple controls, not a single “magic” check.


Validating File Names and Paths

Why file names are dangerous

User-provided file names can contain:

If you simply join a user file name to your upload folder, like:

python
# DANGEROUS
full_path = "/var/app/uploads/" + user_filename

an attacker might choose ../../../../var/app/config.py and make your code write or overwrite sensitive files.


Path traversal protection

To avoid path traversal:

  1. Ignore the directory portion of the user’s name.
  2. Normalize paths and ensure they stay inside a single base directory.
  3. Use your own generated name for storage.

Example pattern in Python:

python
import os
import uuid
BASE_DIR = "/var/app/uploads"
def safe_save(user_filename: str, file_bytes: bytes) -> str:
    # Only keep the last part, remove any directories
    name_only = os.path.basename(user_filename)
    # Extract extension (or empty string)
    _, ext = os.path.splitext(name_only)
    # Generate safe random name, keep extension if allowed
    random_name = f"{uuid.uuid4().hex}{ext}"
    final_path = os.path.join(BASE_DIR, random_name)
    # Extra safety: resolve and verify base path
    real_base = os.path.realpath(BASE_DIR)
    real_final = os.path.realpath(final_path)
    if not real_final.startswith(real_base + os.sep):
        raise RuntimeError("Unsafe file path generated")
    with open(final_path, "wb") as f:
        f.write(file_bytes)
    return random_name

Key ideas:

Never use a user-provided path directly on your filesystem, and never allow .. to influence where you write.


Sanitizing displayed file names

You often still want to show something like the original name to the user in the UI.

Approach:

For example:

Example rule:

Use server-generated names for storage and routing, user-provided names only for display and always HTML-escape them.


Limiting File Size

Why size limits matter

Without limits, an attacker can:

Set limits at multiple levels:

Practical size limit strategies

Examples:

In many frameworks you can:

Python style pseudo code:

python
MAX_BYTES = 5 * 1024 * 1024  # 5 MB
def validate_size(file_obj) -> None:
    # If you can read the size from metadata:
    if file_obj.size > MAX_BYTES:
        raise ValueError("File too large")
    # Or if streaming, count as you read:
    total = 0
    chunks = []
    for chunk in file_obj.chunks():
        total += len(chunk)
        if total > MAX_BYTES:
            raise ValueError("File too large")
        chunks.append(chunk)
    return b"".join(chunks)

Always enforce a maximum allowed size for uploads, do not rely on the client to respect it.


Validating File Types

Why extensions and MIME types are not enough

Attackers can fake:

Only checking ".jpg" or Content-Type: image/jpeg is not reliable.

Use a combination of:

Whitelisting allowed types

Use a whitelist (allow list), never a blacklist:

ContextGood allow list example
Avatars.jpg, .jpeg, .png, .gif
Documents.pdf, .docx, .xlsx
Public downloadsOnly specific known safe file types

Example logic:

python
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"}
ALLOWED_MIME_TYPES = {"image/jpeg", "image/png"}
def validate_file_type(filename: str, mime_type: str, content: bytes) -> None:
    # Extension check
    _, ext = os.path.splitext(filename.lower())
    if ext not in ALLOWED_EXTENSIONS:
        raise ValueError("File type not allowed")
    # MIME type check (from header / environment)
    if mime_type not in ALLOWED_MIME_TYPES:
        raise ValueError("MIME type not allowed")
    # Content-based check for images
    from PIL import Image
    from io import BytesIO
    try:
        img = Image.open(BytesIO(content))
        img.verify()  # verify image structure
    except Exception:
        raise ValueError("Invalid image file")

Do not rely on extensions alone. Validate file content with appropriate libraries when possible.


Avoid executing uploaded files

Whatever the file type:

For example, never:

If you must process files with external tools, see the section on “Handling External Tools” later.


Safe Storage Locations and Permissions

Store files outside the public web root

A common mistake:

If an attacker manages to upload a script that the web server can execute, they may get remote code execution.

More secure:

Directory layout example:

text
/var/www/html/          # public static assets only (your JS, CSS, images)
/var/app/uploads/       # user-uploaded content, not directly served

Keep user-uploaded files outside the web server document root, and expose them only through controlled endpoints.


File and directory permissions

Restrict filesystem permissions so that:

Use OS-level permissions:

Simple principle:


ResourcePermission goal
Upload folderApp user: read/write. Others: minimal access.
Config / keysApp user only. No read for regular users.
Log filesApp user: write. Admin only read.

Using cloud/object storage

Often you will use S3 or similar object storage:

Security tips:

Preventing Overwrites and Conflicts

Unique naming

If you use original names, two users uploading resume.pdf might overwrite each other.

Better:

Common approaches:

StrategyExample
UUID550e8400e29b41d4a716446655440000.pdf
Timestamp + random20240828_101530_ab12cd34.png
Hash-basedsha256-of-content.ext

UUID is simple and effective.

python
import uuid
import os
def generate_storage_name(original_filename: str) -> str:
    _, ext = os.path.splitext(original_filename.lower())
    return f"{uuid.uuid4().hex}{ext}"

Handling duplicates logically

If users should not upload the same file twice, or you want to detect duplicates:

Pseudo code:

python
import hashlib
def file_hash(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()

Note that this is for deduplication, not security. Do not use this alone for integrity or authentication.


Securing File Downloads

Enforce authorization before download

Every download endpoint must check:

Typical data model:

ColumnMeaning
idFile record ID
owner_user_idUser who uploaded or owns the file
stored_nameFile name on disk or object store
original_nameDisplay name
content_typeStored safe MIME type
size_bytesSize
privateBoolean flag

Example pseudo code for download:

python
def download_file(file_id: int, current_user_id: int):
    file = db.get_file(file_id)
    if not file:
        raise NotFound()
    if file.private and file.owner_user_id != current_user_id and not current_user_is_admin():
        raise Forbidden()
    # Stream the file from disk or storage
    return stream_file(file.stored_name, content_type=file.content_type)

Never rely only on a random URL to protect files. Always check permissions on every download.


Content-Disposition and safe names

When sending a file, you often set Content-Disposition:

http
Content-Disposition: attachment; filename="report.pdf"

Do not use the raw user-provided name without sanitizing:

For safety, you can:

Avoiding content sniffing issues

Browsers may try to detect file types themselves, which can cause issues if your headers are wrong.

This reduces risk of the browser executing the content as HTML or JavaScript.


Scanning and Processing Uploaded Files

Antivirus and malware scanning

For systems that accept user content, consider:

Workflow:

  1. User uploads file.
  2. Store file in a temporary location.
  3. Run antivirus scan.
  4. If clean, move to permanent storage. If not, reject and log.

Handling external tools safely

Sometimes you must call external commands for processing, for example:

Risks:

Guidelines:

Example pattern:

python
import subprocess
def resize_image(input_path: str, output_path: str):
    subprocess.run(
        ["convert", input_path, "-resize", "1024x1024>", output_path],
        check=True,
        timeout=10,
    )

Here, convert is called with explicit argument list, not a shell string.


Avoiding Sensitive Data Exposure

Do not store secrets inside uploaded files

If users upload configuration files or logs, ensure you do not accidentally expose:

You cannot fully control what users upload, but you can:

Separate public and private files

Not all uploads are equal.

Typical categories:

TypeExampleAccess pattern
PublicProduct images on an e-commerce siteEveryone can view
PrivateUser invoices, medical docsOnly specific user and admins
InternalLogs, export filesOnly staff or internal systems

Design:

Rate Limiting and Abuse Protection

Throttling upload and download

To prevent denial of service or abuse:

Examples:

Storage quotas

To avoid some users consuming all disk space:

Example model:

ColumnMeaning
user_idOwner
storage_limitMaximum number of bytes allowed
used_bytesCurrent usage

Each time a file is uploaded or deleted, update used_bytes.


Logging and Auditing File Operations

What to log

Record key events:

Do not log sensitive file contents, but do log metadata.


Detecting suspicious activity

Use logs to spot:

You can feed these logs into your monitoring system or SIEM to create alerts.


Common Pitfalls and How to Avoid Them

Typical mistakes

Here are frequent insecure patterns and their safer alternatives:


Insecure patternSafer approach
Saving to uploads/ with user_filename directlyUse basename, random ID, and extension whitelist
Allowing any file typeRestrict to a small, explicit allow list
Reading full file into memory unconditionallyStream uploads and downloads
Serving uploads directly from public web rootStore outside, serve through controlled endpoints
No max size checksEnforce limits at reverse proxy and application
Trusting Content-Type onlyValidate file content with libraries
Using shell commands with user inputUse argument lists and avoid shell=True
No authorization on downloadCheck per-file ownership and permissions

Summary checklist

You can use this short checklist when implementing file handling:

Secure File Handling Checklist

  • All user file names are ignored for paths, only used for display.
  • Path traversal is prevented with basename and base directory checks.
  • Unique server-generated names (UUIDs or similar) are used for storage.
  • Maximum upload size is enforced at multiple levels.
  • Only a small set of file types is allowed, with content validation.
  • Upload directories are outside the public web root.
  • Files and directories use least-privilege filesystem permissions.
  • Download endpoints always check authentication and authorization.
  • External tools are called safely, without shell=True, with timeouts.
  • Logs capture key file events for auditing.

If you follow these rules systematically, your backend will be much more resilient against common file-related attacks.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!