Back to Blog
critical SEVERITY11 min read

Critical File Upload Vulnerability Fixed: How Unrestricted Uploads Put Flask APIs at Risk

A critical security vulnerability was discovered and patched in a Flask REST API endpoint that accepted image file uploads without any validation, size limits, or sandboxing. An attacker could exploit this flaw to upload malicious files disguised as images, potentially leading to remote code execution or exploitation of image-parsing libraries. The fix enforces strict file type validation, magic byte inspection, and upload size limits to close this dangerous attack vector.

O
By orbisai0security
May 3, 2026
#security#flask#python#file-upload#critical-vulnerability#owasp#api-security

Critical File Upload Vulnerability Fixed: How Unrestricted Uploads Put Flask APIs at Risk

Severity: 🔴 Critical | CVE Category: Unrestricted File Upload | Affected Component: utils/flask_rest_api/restapi.py


Introduction

File upload functionality is one of the most deceptively dangerous features a web application can offer. What looks like a simple "choose your profile picture" button can become a wide-open door for attackers if not implemented carefully. This week, a critical severity vulnerability was patched in a Flask REST API endpoint that accepted image uploads without performing any meaningful validation.

If you're a developer building APIs that accept user-supplied files — even something as seemingly harmless as images — this post is required reading. Unrestricted file upload vulnerabilities consistently appear in the OWASP Top 10 and are responsible for some of the most devastating real-world breaches. Let's break down exactly what went wrong, how it was fixed, and how you can protect your own applications.


The Vulnerability Explained

What Was the Problem?

The vulnerable endpoint in utils/flask_rest_api/restapi.py (line 25) accepted image file uploads via Flask's request.files['image'] interface. The code would take whatever file a user submitted and pass it directly to an image processing pipeline — likely using PIL (Pillow) or OpenCV — without performing any of the following critical checks:

  • MIME type inspection — Is the file actually an image?
  • Magic byte verification — Do the first bytes of the file match a known image format?
  • Extension allowlisting — Is the file extension .jpg, .png, or .gif?
  • File size limits — Could an attacker upload a 10GB file to exhaust server resources?
  • Sandboxing — Is the file processed in an isolated environment?

In short: the server trusted the client completely. And in security, that's almost always a fatal mistake.

Technical Deep Dive

Here's a simplified representation of what the vulnerable code pattern looked like:

# VULNERABLE CODE (before fix)
from flask import Flask, request
from PIL import Image
import io

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_image():
    # ⚠️ No validation whatsoever!
    image_file = request.files['image']

    # Directly processing whatever the user sent
    img = Image.open(image_file)
    img.save('/uploads/' + image_file.filename)

    return {'status': 'uploaded'}, 200

This code is dangerous for several compounding reasons:

  1. No type checking: The server assumes the uploaded file is an image simply because it was sent to an image upload endpoint.
  2. Direct filename usage: Using image_file.filename from user input opens the door to path traversal attacks (e.g., uploading a file named ../../etc/cron.d/malicious).
  3. No size restriction: An attacker can send gigabytes of data, causing a Denial of Service (DoS).
  4. Unvalidated parsing: Passing arbitrary data to Image.open() exposes the server to image parser exploits.

How Could This Be Exploited?

Let's walk through several realistic attack scenarios:

🎭 Scenario 1: The Polyglot File Attack

A polyglot file is a file that is simultaneously valid in two different formats. An attacker can craft a file that:
- Passes a naive extension check (it ends in .jpg)
- Is actually a valid PHP or Python script when interpreted by a server

# Attacker crafts a polyglot: valid JPEG header + embedded PHP payload
# The file looks like an image but contains executable code
printf '\xFF\xD8\xFF\xE0<?php system($_GET["cmd"]); ?>' > evil.jpg

# Upload it to the vulnerable endpoint
curl -X POST http://target.com/upload \
  -F "image=@evil.jpg"

If the server later serves this file from a location where scripts can be executed, the attacker achieves Remote Code Execution (RCE).

💣 Scenario 2: Image Parser Exploitation (CVE-style)

Image parsing libraries like PIL/Pillow and OpenCV have had numerous CVEs over the years:

  • CVE-2021-34552 — Pillow buffer overflow via crafted image
  • CVE-2022-22817 — Pillow PIL.ImageMath.eval arbitrary code execution
  • CVE-2023-44271 — Pillow uncontrolled resource consumption

By uploading a specially crafted malformed image, an attacker can trigger these vulnerabilities in the parsing library itself, potentially achieving code execution inside the server process — no extension tricks needed.

📁 Scenario 3: Path Traversal via Filename

# If the server uses the original filename:
image_file.filename  # Attacker supplies: "../../app/templates/index.html"

# The server might overwrite critical application files!
img.save('/uploads/' + image_file.filename)
# Resolves to: /app/templates/index.html  ← Overwritten!

🌊 Scenario 4: Denial of Service via Upload Bomb

Without size limits, an attacker can upload a "zip bomb" or a massive file that exhausts disk space and memory:

# Upload a 10GB file of zeros disguised as an image
dd if=/dev/zero bs=1M count=10240 | curl -X POST http://target.com/upload \
  -F "image=@-;filename=bomb.jpg"

Real-World Impact

The business impact of this vulnerability class is severe:

Impact Description
Remote Code Execution Attacker gains full control of the server
Data Exfiltration Access to databases, credentials, source code
Service Disruption DoS via resource exhaustion
Lateral Movement Server becomes a pivot point into internal network
Compliance Violations GDPR, HIPAA, PCI-DSS breaches

The Fix

What Changes Were Made?

The patch to utils/flask_rest_api/restapi.py introduces multiple layers of defense, following the defense-in-depth principle. Here's what a properly secured version of this endpoint looks like:

# SECURE CODE (after fix)
import os
import io
import uuid
import magic  # python-magic for MIME detection
from flask import Flask, request, abort
from PIL import Image
from werkzeug.utils import secure_filename

app = Flask(__name__)

# Configuration constants
MAX_CONTENT_LENGTH = 5 * 1024 * 1024  # 5MB hard limit
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp'}
ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'}
ALLOWED_MAGIC_BYTES = {
    b'\xFF\xD8\xFF',           # JPEG
    b'\x89PNG\r\n\x1a\n',     # PNG
    b'GIF87a', b'GIF89a',     # GIF
    b'RIFF',                   # WebP (followed by WEBP)
}

UPLOAD_FOLDER = '/secure/uploads'

def allowed_extension(filename: str) -> bool:
    """Check if file extension is in the allowlist."""
    return (
        '.' in filename and
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
    )

def allowed_mime_type(file_stream) -> bool:
    """Verify MIME type using python-magic (reads actual file bytes)."""
    header = file_stream.read(2048)
    file_stream.seek(0)  # Reset stream position
    mime = magic.from_buffer(header, mime=True)
    return mime in ALLOWED_MIME_TYPES

def validate_image_integrity(file_stream) -> bool:
    """Attempt to open and verify the file is a valid image."""
    try:
        file_stream.seek(0)
        with Image.open(file_stream) as img:
            img.verify()  # Verify it's not corrupted/malicious
        file_stream.seek(0)
        return True
    except Exception:
        return False

@app.route('/upload', methods=['POST'])
def upload_image():
    # ✅ Check file is present in request
    if 'image' not in request.files:
        abort(400, description="No image file provided")

    image_file = request.files['image']

    if image_file.filename == '':
        abort(400, description="Empty filename")

    # ✅ Validate file extension against allowlist
    if not allowed_extension(image_file.filename):
        abort(400, description="File type not permitted")

    # ✅ Validate actual MIME type (not just Content-Type header)
    if not allowed_mime_type(image_file.stream):
        abort(400, description="File content does not match an allowed image type")

    # ✅ Validate image integrity (catches malformed/exploit images)
    if not validate_image_integrity(image_file.stream):
        abort(400, description="Invalid or corrupted image file")

    # ✅ Sanitize filename to prevent path traversal
    safe_filename = secure_filename(image_file.filename)

    # ✅ Generate a random filename to prevent enumeration/overwrite
    file_ext = safe_filename.rsplit('.', 1)[1].lower()
    random_filename = f"{uuid.uuid4().hex}.{file_ext}"

    # ✅ Save to designated upload directory
    save_path = os.path.join(UPLOAD_FOLDER, random_filename)
    image_file.stream.seek(0)
    image_file.save(save_path)

    return {'status': 'uploaded', 'id': random_filename}, 200

@app.errorhandler(413)
def too_large(e):
    return {'error': 'File too large. Maximum size is 5MB.'}, 413

How Does the Fix Solve the Problem?

Let's map each fix to the attack scenarios we discussed:

Defense Layer What It Prevents
MAX_CONTENT_LENGTH = 5MB DoS via upload bombs
Extension allowlist Obvious malicious file types
magic MIME type check Polyglot files with wrong extensions
img.verify() integrity check Malformed images targeting parser CVEs
secure_filename() Path traversal via crafted filenames
uuid4() random filenames Filename enumeration and targeted overwrites

Why Magic Bytes Matter More Than Extensions

This is a critical concept worth emphasizing. File extensions are user-controlled and completely untrustworthy. A file named evil.jpg can contain anything. Magic bytes — the first few bytes of a file that identify its format — are much harder to fake while maintaining a valid attack payload.

# These are the first bytes of real image files:
JPEG:  FF D8 FF E0 ...
PNG:   89 50 4E 47 0D 0A 1A 0A ...
GIF:   47 49 46 38 39 61 ...  ("GIF89a")

# python-magic reads these bytes directly from the file content
# NOT from the filename or Content-Type header
mime = magic.from_buffer(header, mime=True)

Why img.verify() Adds Another Layer

Even if a file passes MIME type checking, it might still contain embedded exploit payloads. Calling Image.verify() forces PIL to parse the image structure and raises an exception if anything looks malformed. This doesn't guarantee safety against all parser bugs, but it significantly raises the bar for attackers.


Prevention & Best Practices

Checklist for Secure File Upload Endpoints

Use this checklist whenever you implement file upload functionality:

 VALIDATION
 Allowlist permitted file extensions (never use a denylist)
 Check MIME type using magic bytes, not just Content-Type header
 Validate file integrity by attempting to parse/open it
 Reject files with multiple extensions (e.g., evil.php.jpg)

 STORAGE
 Store uploads outside the web root (not publicly accessible by default)
 Generate random filenames  never use user-supplied filenames
 Use secure_filename() or equivalent to sanitize any filename components
 Consider a dedicated object storage service (S3, GCS, Azure Blob)

 SIZE & RATE LIMITING
 Enforce maximum file size at the framework level
 Implement rate limiting on upload endpoints
 Consider total storage quotas per user

 PROCESSING
 Process files in an isolated environment (container, sandbox)
 Use up-to-date versions of image processing libraries
 Consider re-encoding images (strip metadata, re-render) to neutralize payloads
 Scan uploaded files with antivirus/malware detection

 SERVING
 Serve user-uploaded files from a separate domain (prevents same-origin attacks)
 Set Content-Disposition: attachment for downloads
 Set proper Content-Type headers when serving files
 Implement access controls  who can access what files?

The Nuclear Option: Image Re-encoding

For the highest level of security, don't just validate the uploaded image — re-encode it entirely. This strips any embedded payloads and ensures you're serving a clean, freshly generated image:

from PIL import Image
import io

def sanitize_image(file_stream) -> bytes:
    """
    Re-encode image to strip any embedded payloads.
    The output is a freshly rendered image with no original metadata.
    """
    with Image.open(file_stream) as img:
        # Convert to RGB to strip alpha channel tricks
        if img.mode not in ('RGB', 'L'):
            img = img.convert('RGB')

        # Re-encode to a clean buffer
        output = io.BytesIO()
        img.save(output, format='JPEG', quality=85)
        return output.getvalue()

This approach is used by major platforms like Facebook and Google — they never serve your original upload directly.

Relevant Security Standards

  • OWASP: File Upload Cheat Sheet
  • CWE-434: Unrestricted Upload of File with Dangerous Type
  • CWE-22: Path Traversal
  • CWE-400: Uncontrolled Resource Consumption
  • OWASP Top 10 A04:2021: Insecure Design

Tools to Detect This Vulnerability

# Static Analysis
bandit -r your_flask_app/          # Python security linter
semgrep --config=p/python          # Pattern-based code scanning

# Dynamic Testing
# OWASP ZAP - active scanner with file upload tests
# Burp Suite - manual testing with file upload plugins

# Dependency Scanning (for known parser CVEs)
pip-audit                          # Check for vulnerable Python packages
safety check                       # Alternative dependency scanner

A Note on Defense in Depth

No single validation check is foolproof. New image parser CVEs are discovered regularly. Magic byte checks can theoretically be bypassed in edge cases. img.verify() doesn't catch every malformed image.

That's why defense in depth is the right mental model: each layer of validation independently raises the cost and complexity of an attack. An attacker who bypasses your extension check still faces MIME validation. One who crafts a valid MIME type still faces integrity checking. One who passes all checks still encounters a random, non-executable filename in a non-web-accessible directory.

Security is not a single wall — it's a series of gates.


Conclusion

The unrestricted file upload vulnerability patched in utils/flask_rest_api/restapi.py is a textbook example of how seemingly simple functionality can hide critical security risks. By accepting user-uploaded files without validation, the application was exposed to remote code execution, denial of service, path traversal, and image parser exploitation.

The fix applies multiple independent layers of defense:
- Allowlist-based extension validation to block obvious threats
- Magic byte MIME inspection to catch polyglot files
- Image integrity verification to detect malformed exploit payloads
- Filename sanitization and randomization to prevent path traversal and enumeration
- Hard file size limits to prevent resource exhaustion

Key Takeaways

🔑 Never trust user input — including filenames, MIME types, and file contents

🔑 Validate at multiple layers — extension + MIME + integrity check

🔑 Use magic bytes, not Content-Type headers or extensions, to determine file type

🔑 Isolate and sanitize — store uploads outside the web root, use random filenames

🔑 Keep dependencies updated — image parser CVEs are real and actively exploited

File upload security is not glamorous, but getting it wrong can be catastrophic. Invest the time to implement it correctly from the start — your future self (and your users) will thank you.


This vulnerability was identified and fixed as part of an automated security scanning pipeline. Continuous security scanning helps catch critical issues before they reach production. Consider integrating tools like OrbisAI Security into your CI/CD pipeline for automated vulnerability detection.


Further Reading:
- OWASP File Upload Cheat Sheet
- Portswigger: File Upload Vulnerabilities
- HackTricks: File Upload
- Pillow Security Advisories

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13760

Related Articles

critical

Stack Buffer Overflow in MapScale: How Five Unsafe sprintf Calls Created a Critical Vulnerability

A critical stack-based buffer overflow vulnerability was discovered and patched in `src/mapscale.c`, where five unbounded `sprintf` calls wrote formatted output into fixed-size stack buffers without any bounds checking. An attacker controlling unit text strings could overflow the stack buffer, potentially overwriting the function return address and achieving arbitrary code execution. The fix replaces dangerous `sprintf` calls with their bounds-checked counterparts, eliminating the overflow risk

critical

Heap Buffer Overflows in YAML Parser: How Unchecked memcpy Calls Create Critical Attack Vectors

A critical heap buffer overflow vulnerability was discovered and patched in the YAML parser embedded within an Android VPN application, where five unvalidated `memcpy` calls could allow an attacker to corrupt heap memory by supplying a crafted YAML configuration file. This class of vulnerability is particularly dangerous because it can lead to arbitrary code execution or application crashes in security-sensitive contexts. The fix adds proper bounds validation before each copy operation, eliminat

critical

Critical Buffer Overflow Fixed: When "Safe" Functions Aren't Safe

A critical vulnerability in DeepSkyStackerKernel's StackWalker.cpp was silently replacing bounds-checking string functions with their unsafe counterparts via preprocessor macros, exposing the entire codebase to buffer overflow attacks. This fix removes the dangerous macro definitions that discarded buffer size arguments, restoring the intended memory safety protections across all call sites. Understanding how this subtle macro trick works is essential for any C/C++ developer working with string