The deposit slot that takes any parcel
Watch it happen
Mallory posts a disguised tool through the deposit slot. The bank stores it on the public shelf, where it gets used, and a trick filename puts it in the wrong place.
The bank has a deposit slot for parcels, like profile pictures or documents. Alice walks up to post one. Mallory follows her in.
In bank terms and app terms
| In the bank | In your app |
|---|---|
Are you exposed?
Your upload route saves files using the filename the browser sent. Uploaded files are stored in a folder your web server serves directly, or accept HTML and SVG files. There is no maximum size and no list of allowed file types.
Allow only the types you need, check the real content type, and set a maximum size. Generate your own random filename and never use the user’s name or path. Store files outside the web root (private bucket), serve them with a safe content type, and never serve user HTML or SVG from your own domain.
It has happened to real companies
ImageTragick (2016) — CVE-2016-3714 in ImageMagick allowed remote code execution via crafted image files. Any service that accepted user-uploaded images and processed them with ImageMagick was vulnerable.
What it looks like in code
// GOOD: Allowlist extensions, cap the size and check the real bytes (file.type comes from the client)
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"]
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get("file") as File
const ext = path.extname(file.name).toLowerCase()
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return NextResponse.json({ error: "File type not allowed" }, { status: 400 })
}
if (file.size > 5 * 1024 * 1024) {
return NextResponse.json({ error: "File too large" }, { status: 413 })
}
// Check the file's magic bytes, not its claimed content type
const bytes = new Uint8Array(await file.arrayBuffer())
const isPng = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47
const isJpeg = bytes[0] === 0xff && bytes[1] === 0xd8
const isWebp = bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50 // "WEBP" after "RIFF"
if (!(isPng || isJpeg || isWebp)) {
return NextResponse.json({ error: "Invalid file content" }, { status: 400 })
}
// Generate a random filename (prevents path traversal)
const safeFilename = `${crypto.randomUUID()}${ext}`
// ... save with safeFilename, outside the web root
}
Ask your AI tool to fix it
Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.
Harden all file upload endpoints. Allowlist extensions and MIME types and verify the real file signature, enforce a maximum size, ignore the client-supplied filename and generate a random one, store files in private storage outside the web root, serve them with Content-Disposition and X-Content-Type-Options: nosniff, and reject HTML and SVG unless sanitized. List the changes.Where this comes from
The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.
- File Upload Cheat SheetOWASPAllowlist extensions, check file signature, size limits, random filenames, store outside webroot
- File upload vulnerabilitiesPortSwigger Web Security AcademyClient-supplied Content-Type is easy to fake; uploaded HTML or SVG can run script (stored XSS)
- X-Content-Type-OptionsMDN Web Docsnosniff stops browsers guessing a content type for served files
- Content-DispositionMDN Web DocsContent-Disposition: attachment makes the browser download the file instead of displaying it
- ImageTragick (ImageMagick vulnerabilities)ImageTragickCVE-2016-3714: remote code execution when processing user-submitted images with ImageMagick
- CVE-2016-3714Ubuntu SecurityAffected ImageMagick versions (before 6.9.3-10 and 7.0.1-1) allow code execution via crafted image
Spot a mistake or something out of date? Report a correction · How these guides are made and checked
The free, open-source scanner checks for most of the problems in these guides.