Lion Cipher Lab
Exploit and fix an insecure file upload (in your own lab)
- AppSec / DevSecOps
- Intermediate
- ~50 min
- Free · no account required
Why it matters
Unrestricted file upload is one of the highest-impact web bugs because the payoff can be direct code execution on the server: if an app lets a user upload a file and stores it somewhere the web server will EXECUTE (a .php, .jsp, .aspx in a served directory), the attacker uploads a script and then requests it to run their code as the app. Even when execution is not possible, a trusted-name upload enables path traversal (choosing where the file is written, overwriting other files), stored cross-site scripting via an uploaded HTML or SVG, and content-type confusion. The root cause is trusting…
What you'll learn
- explaining unrestricted file upload (CWE-434): an app that trusts the client-supplied filename, extension, and Content-Type header and stores the file in a readable or EXECUTABLE location lets an attacker place a file that can be fetched back or run
- building a tiny vulnerable Flask upload endpoint on your own machine that saves request.files[...] to a web-served folder using the raw client filename with no validation
- exploiting it locally with a PATH-TRAVERSAL filename (curl ...;filename=../../escape.txt) and watching the saved file land OUTSIDE the upload directory — proof the app writes wherever the client names
- exploiting it locally with a CONTENT-TYPE / EXTENSION MISMATCH — sending a plain-text file as shell.php with a forged Content-Type: image/png — and seeing it accepted, proving both the header and the extension are client-controlled theatre
- fixing it in layers: never trust the client filename (generate a random server-side name; know secure_filename and its limits), validate the ACTUAL bytes not the claimed type (Pillow re-encode / real format + an extension ALLOWLIST), store OUTSIDE the web root on a non-executable path, and enforce MAX_CONTENT_LENGTH
- re-sending the identical malicious uploads against the fixed app and confirming they are rejected while a legitimate PNG still uploads and is served safely
- explaining why blocklisting extensions is a losing game (double extensions, case, null bytes, .phtml, polyglots) and why removing execution + allowlisting + content validation is the durable fix
- recognizing that a 'looks like a PNG' magic-byte check is necessary but NOT sufficient (polyglots) — the real fix is that the storage location cannot execute anything, and the defense-in-depth stack: validate + rename + store-outside-webroot + AV scan + size limit + separate domain/CDN for user content
Steps in this lab
- What insecure file upload is — the vocabulary and the ground rules
- Set up: confirm Python, make a folder, install Flask and Pillow
- Build the VULNERABLE app (it trusts the client's filename)
- Prove the two failures (a traversal filename, and forged type/extension)
- Why blocklisting extensions (and trusting magic bytes alone) is the wrong fix
- Fix it: rename, validate real bytes, store outside the web root, cap size
- What you did — and the reflex to keep