Skip to main content

uploadshield

PyPI Python License Dependencies OWASP

Content-aware validation and sanitization of file uploads for Django, Flask and FastAPI.

Web frameworks hand you the filename and Content-Type of an upload as if they were facts. They are whatever the client sent: a PHP web shell named cat.png with Content-Type: image/png arrives as an image in Django, Flask and FastAPI alike. uploadshield judges the bytes instead. It detects the real format, checks it against what the endpoint accepts, looks inside documents and archives for active content, and can rewrite images so that hidden payloads and metadata are destroyed.

Try it in 10 seconds

No code needed; uv runs it straight from PyPI:

$ uvx uploadshield scan uploads/
uploads/
  ✗ avatar.png  PHP script · 30 bytes
      type_not_allowed  PHP script files are not allowed.
      embedded_code  The file contains embedded code.  <?php
  ? backup.7z  7-Zip archive · 187 bytes
      type_not_allowed  Its contents cannot be inspected: unpack it and scan the files, or add --clamav.
  ! holiday.png  JPEG image · 14.6 KB
      extension_mismatch  The file extension '.png' does not match its content (JPEG image).
  ✗ invoice.pdf  PDF document · 293 bytes
      pdf_active_content  The PDF contains active content such as scripts or embedded files.  JS, JavaScript
  ✓ notes.txt  Plain text · 14 bytes
  ✓ report.pdf  PDF document · 180 KB

6 files in 0.0 s: 2 clean, 2 dangerous, 1 refused, 1 not verified.

uploadshield validate FILE --policy NAME shows exactly why a policy accepts or refuses a file. See the command line guide.

What it protects against

  • Disguised files: scripts and executables named .png or .pdf. The format comes from the content, never from the name.
  • Dangerous names: shell.php.jpg, .htaccess, ../../etc/passwd, CON.txt, bidi-override tricks, and their percent-encoded and fullwidth variants.
  • Active documents: PDFs with JavaScript or embedded files, Office files with macros, DDE fields or remote templates, SVGs with scripts, XML with entity bombs.
  • Archive attacks in ZIP and tar (.tar.gz, .tar.bz2, .tar.xz): bombs, zip slip, links, deeply nested archives, and members a server would run once extracted.
  • Hidden payloads: PHP in image metadata, polyglots such as GIFAR, CSV cells that run commands through DDE (=cmd|' /C calc'!A0).
  • Malware, with ClamAV or your own YARA rules, and optionally Google's Magika model for scripts disguised as data.

It fails closed: a check that crashes, a file too large to inspect, an encrypted file or an unreachable virus scanner means a refusal. Image decoding needs Pillow, the images extra (uvx --from "uploadshield[images]" uploadshield scan ...); without it the command line warns that image checks are inactive. The checks reference lists everything each check catches, and what it does not.

Add it to your app

uv add "uploadshield[django,images]"    # or [flask,images] or [fastapi,images]

Django: every file is checked while the request body streams in, in views, forms, the admin and Django REST framework.

# settings.py
INSTALLED_APPS += ["uploadshield.django"]
FILE_UPLOAD_HANDLERS = ["uploadshield.django.handlers.ValidatingUploadHandler"]

# urls.py (root URLconf): answer refusals with 413/415/400, as JSON or HTML
handler400 = "uploadshield.django.views.bad_request"

Flask: every multipart request is checked before it reaches a view.

from flask import Flask

from uploadshield.flask import UploadShield

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024  # Werkzeug parses the body first
UploadShield(app)

FastAPI: dependencies validate and sanitize uploads; all_files() covers the whole app.

from typing import Annotated

from fastapi import Depends, FastAPI, UploadFile

from uploadshield.fastapi import UploadShield

shield = UploadShield()
app = FastAPI(dependencies=[Depends(shield.all_files())])


@app.post("/avatar")
async def avatar(
    file: Annotated[UploadFile, Depends(shield.file("sanitized_images"))],
): ...  # re-encoded, renamed, with the content type detected from the bytes

That's it: every upload is now checked against the built-in default policy (common images, PDF, Office and OpenDocument files, text and CSV, audio and video, up to 50 MB). Your code only receives files that passed; the others are answered with 413, 415 or 400. On an existing site, start with "MODE": "report" to log would-be refusals without blocking anything.

The guides cover every option: Django, Flask, FastAPI.

Decide what to accept

A policy says which files an endpoint accepts. Pick a built-in one, or derive your own:

from uploadshield import MB, Policy, presets

AVATARS = presets.SANITIZED_IMAGES.replace(name="avatars", max_size=2 * MB)
INVOICES = Policy(
    name="invoices",
    allowed_types={"application/pdf", "image/png", "image/jpeg"},
    max_size=5 * MB,
)
Built-in policy Accepts Size limit
default everything below 50 MB
images JPEG, PNG, GIF, WebP 10 MB
sanitized_images the same, re-encoded and randomly renamed 10 MB
pdf PDF 25 MB
documents PDF, Word, Excel, PowerPoint, OpenDocument, text, CSV 25 MB
media common audio and video formats 500 MB

Use one per view (@validate_uploads("pdf") in Django and Flask, shield.file("pdf") in FastAPI), per URL pattern, or per form field. The policy guide covers allowed types, filename rules, tuning checks, virus scanners and sanitizers.

How it works

The client's upload is parsed by Django, Flask or FastAPI, which hands every file to uploadshield. uploadshield checks size, filename, the type proven by the bytes, the allow-list and the content, then sanitizes. Passing files reach your view; refused files never do, and the client receives a 400, 413 or 415.

  • Cheapest checks first. A forbidden name is refused before any content is read; expensive parsers only see files that passed the cheap checks.
  • Your code only sees files that passed, sanitized if the policy says so, with the content type detected from the bytes.
  • Every refusal is answered and recorded: a stable code, a 400/413/415 response, a structured log record and a signal or callback.

Concepts explains the model; Architecture how it is built.

Documentation

Page What you find there
Concepts how uploadshield works, in one read
Django · Flask · FastAPI setup, every option, refusals, testing
Command line scan, validate, JSON output for CI
Policies choosing and configuring what to accept
Checks and sanitizers every check with all its options
Refusals codes, status codes, messages, logs
Python API using the engine directly, custom checks and types
All docs the full map, and llms.txt for AI agents

Security and compatibility

  • Mapped to OWASP: every recommendation of OWASP's File Upload Cheat Sheet and every test case of its Unrestricted File Upload page, with its coverage and the tests that prove it, is in docs/OWASP.md.
  • Supported: Python 3.11+, Django 4.2+, Flask 2.3+, FastAPI 0.110+.
  • No required dependencies. Pillow, YARA, Magika and libmagic are optional extras.
  • Typed (py.typed), with about 2500 tests including property-based fuzzing.
  • Coming from django-middleware-fileuploadvalidation 1.x? See Migrating from 1.x.

Contributing: see Development. License: Apache-2.0.

Metadata

Release files for uploadshield 2.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uploadshield 2.2.0
File Size Uploaded Provenance Actions
uploadshield-2.2.0.tar.gz 120.2 kB Without provenance Details for uploadshield-2.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for uploadshield 2.2.0
File Interpreter ABI Platform Actions
uploadshield-2.2.0-py3-none-any.whl Python 3 none any Details for uploadshield-2.2.0-py3-none-any.whl

Total release size: 262.6 kB

Release files / uploadshield-2.2.0.tar.gz

Download URL uploadshield-2.2.0.tar.gz
Size 120.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9f6a8be11de3dd68c1ca1a5a9f5fdc7aabbb8e5423b01b9d1f955ae5f80c65a1
BLAKE2b-256 checksum
How to use checksums
cd81758f519768b0d71585724ddd56ff06846414c87437e98bef057d0203ec09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / uploadshield-2.2.0-py3-none-any.whl

Download URL uploadshield-2.2.0-py3-none-any.whl
Size 142.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b62591209361138ca7a404943129335ec0097a2c838d3c5c127292c5863c14a9
BLAKE2b-256 checksum
How to use checksums
425fa1dffe9277da09011c4531b8f0dea04b1c035304a8ce196a6be417bbff8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for uploadshield 2.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page