Choosing a photo looks simple: the browser sends a file and the server stores it.
However, its name, declared type, and bytes all came from the client, so the server
cannot trust them automatically.
A safe upload is not one complicated check but a few understandable filters. Validate
the file first, generate a safe name second, and only then pass it to local or cloud
storage.
Layered validation
Think of this as a checkpoint: each check answers one question. The resulting code is
easier to read, test, and reuse in another project.
For an image, check in this order:
- a file was actually supplied;
- the declared MIME type is allowlisted;
- the bytes read do not exceed the limit;
- signature and decoding confirm the format;
- the server generates the storage key instead of trusting
filename.
Never join filename="../secret.txt" to the upload directory. Generate a key:
from uuid import uuid4
key = f"images/{uuid4().hex}.webp"
FastAPI UploadFile
UploadFile requires python-multipart. It provides metadata and a file interface,
and can spool larger data from memory to disk.
from fastapi import HTTPException, UploadFile
MAX_BYTES = 5 * 1024 * 1024
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
async def read_limited(upload: UploadFile) -> bytes:
if upload.content_type not in ALLOWED_TYPES:
raise HTTPException(415, "Unsupported image type")
data = await upload.read(MAX_BYTES + 1)
if len(data) > MAX_BYTES:
raise HTTPException(413, "Image is too large")
if not data:
raise HTTPException(400, "Image is empty")
return data
Then verify magic bytes or decode the image with a suitable library. Never trust an
extension alone.
The extra byte in MAX_BYTES + 1 reveals an exceeded limit without reading the whole
file. For very large uploads, read smaller chunks instead.
Store uploads outside templates and executable code. Authorize private downloads,
delete objects by their saved key, and consider a matching request-body limit at the
reverse proxy. Test valid, empty, oversized, disguised, and ../cover.png uploads.
Begin with the happy path and change exactly one condition per test. Responses 415
for type, 413 for size, and 400 for an empty file make each rejection clear.
๐ฌ Comments (0)
No comments yet
Be the first to share your opinion about this article!