uploadshield
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
.pngor.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 |
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
- 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.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| uploadshield-2.3.0.tar.gz | 121.9 kB | Without provenance | Details for uploadshield-2.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| uploadshield-2.3.0-py3-none-any.whl | Python 3 | none | any | Details for uploadshield-2.3.0-py3-none-any.whl |
Total release size: 266.0 kB
Release files / uploadshield-2.3.0.tar.gz
| Download URL | uploadshield-2.3.0.tar.gz |
|---|---|
| Size | 121.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6be2aa17e5f86a9a815ef3c320b65cd39aa3e23f613f350aa1047acd16fa1dab
|
|
BLAKE2b-256 checksum How to use checksums |
fa54f90dd99bf73a3a99e77a47b0e7ab7146f8ffc6a0de201f8a13784a084027
|
| 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.3.0-py3-none-any.whl
| Download URL | uploadshield-2.3.0-py3-none-any.whl |
|---|---|
| Size | 144.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
58f7a324563eb72eb86fb54a14625f2c00bf15c566d4111e2fdf15c8600b42d5
|
|
BLAKE2b-256 checksum How to use checksums |
752b7a4b39bf6b6342fa3682674c6e0261b73ada9d416f0823d7383e25df245f
|
| 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.3.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.