
unrestricted file upload leads to remote code execution
f.save(f'/var/www/app/uploads/{f.filename}')the profile photo that wasn't a photo
someone uploads a "profile picture" to your app. it's five lines of php. they visit the url where it landed and suddenly they're running commands as www-data on your server. no exploit chain, no fancy zero day, just one line of code that trusted things it never should have trusted.
this is the unrestricted file upload bug, and it's still one of the most common ways attackers get a foothold. let's break down exactly what went wrong so you can check your own upload code today.
the line of code doing all the damage
f.save(f'/var/www/app/uploads/{f.filename}')
looks harmless. it's one line. it's also three separate mistakes stacked on top of each other.
mistake one: trusting the filename. f.filename comes straight from the user's browser. nothing stops someone from naming their file whatever they want, including things like ../../etc/passwd if you don't sanitize path traversal, or just a name that collides with something already on your server.
mistake two: trusting the extension. nothing here checks what kind of file this actually is. the code assumes if it looks like an image upload form, the file is an image. an attacker can rename shell.php to photo.jpg.php or just upload shell.php directly if there's no extension allowlist at all.
mistake three: saving it where the server will execute it. this is the one that turns a bad upload into remote code execution. if your uploads folder lives inside the web root and your server is configured to execute php (or any script type) from that folder, then visiting the file's url doesn't just download it, it runs it.
any one of these three mistakes alone is a problem. all three together is how someone gets a shell.
fix one: check the actual contents, not the extension
extensions are a suggestion. file content is the truth. use a library that inspects the actual bytes of the file to confirm it's really an image before you save anything.
if imghdr.what(f.stream) not in ('jpeg','png'):
abort(400)
this checks the file's magic bytes, the actual binary signature that identifies file type, instead of trusting whatever the filename claims. a php shell renamed to photo.jpg will fail this check every time because it doesn't have valid image data inside it.
fix two: never trust the filename
even if the content check passes, don't save the file under the name the user gave you. generate your own.
name = f'{uuid4().hex}.{ext}'
this generates a random, unpredictable filename and pairs it with an extension your own code chose, not one the attacker supplied. no path traversal, no collisions, no sneaky double extensions. the user's original filename can get stored in a database column if you need it for display, but it should never touch your filesystem path.
fix three: make the upload folder incapable of running code
this is the fix that matters most because it's the one that stops the attack even if the first two somehow fail. tell your web server to never execute scripts from the uploads directory, period.
location /uploads/ {
location ~ \.php$ { deny all; }
}
this nginx config block says: inside the uploads folder, if a request tries to hit anything ending in .php, deny it outright. even if a malicious php file somehow lands in that folder, the server refuses to execute it. it just serves it as plain text or denies the request entirely. this single server config change turns "attacker uploaded a shell" into "attacker uploaded a useless text file."
the takeaway
you don't need all three fixes to stop this attack, you need any one of them done right. check the content so fake images can't pretend to be real ones. randomize filenames so attackers can't control where things land or what they're called. and lock down your upload directory at the server level so even a successful bad upload has nowhere to execute.
go look at your own upload handling code today. find the line that saves the file. if it looks anything like f.save(f'/uploads/{f.filename}'), you've got homework. fix it before someone else finds it for you.