Today’s goal sounded harmless:
“Let users upload a profile picture.”
Every website has this feature. Social media, college portals, job sites, forums… all of them.
And that is exactly why it is dangerous.
Because developers (including me) usually think:
It’s just an image upload. What could go wrong?
Turns out… a lot.
Step 1 — The Innocent Feature
I created a small Flask application that allows a user to upload a file and view it back.
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
filepath = os.path.join("uploads", file.filename)
file.save(filepath)
return f"File uploaded: <a href='/files/{file.filename}'>View</a>"
It worked perfectly.
Upload image → open image → done.
Or so I thought.
The First Strange Bug
When I uploaded a PNG image, my server crashed:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89
I was reading the file using:
open(filepath).read()
The problem?
I tried to read an image as text.
Images are binary data. Python attempted to interpret the raw bytes as UTF-8 characters and failed immediately.
Fixing it required proper file serving:
from flask import send_from_directory
@app.route('/files/<filename>')
def files(filename):
return send_from_directory("uploads", filename)
The crash disappeared.
But the real problem had just begun.
Step 2 — Breaking My Own Website
Then I asked a dangerous question:
What if the uploaded file is not an image?
I created a file:
evil.html
<h1>You have been hacked</h1>
<script>
document.body.innerHTML += "<p>This page is executing attacker code.</p>";
</script>
I uploaded it.
Then opened:
http://127.0.0.1:5000/files/evil.html
And the browser executed it.
My website was now hosting attacker-controlled webpages.
This is called:
Stored XSS (Cross-Site Scripting)
The server trusted the uploaded file and served it back to visitors.
The browser trusted the server and executed the script.
My application became a delivery system for malicious code.
Why This Is Serious
An attacker can upload a malicious page and send the link to a victim:
"Hey check your profile picture"
The victim opens it.
The script can:
- steal session cookies
- perform actions as the user
- send private data to attacker
- access admin panels
The website is no longer just vulnerable.
It is now attacking its own users.
Another Hidden Disaster
I was running Flask with:
app.run(debug=True)
Flask debug mode exposes the Werkzeug interactive debugger.
If this server were public, an attacker could execute Python commands remotely.
That means:
Remote Code Execution (RCE)
A simple file upload feature could have turned into full server compromise.
Step 3 — The Real Fix (Hardening the Upload System)
Fixing file uploads is not just “allow only .png”.
Attackers rename files:
shell.php → shell.png
evil.html → cat.jpg
So I implemented real defenses.
1. Sanitize the Filename
Attackers can upload:
../../../../app.py
I used:
from werkzeug.utils import secure_filename
safe_name = secure_filename(file.filename)
This prevents path traversal and overwriting server files.
2. Randomize the Name
import uuid
filename = str(uuid.uuid4()) + "_" + safe_name
This prevents overwriting another user’s file.
3. Validate the Actual File Type
Instead of trusting extensions, I verified the file signature:
import imghdr
file_type = imghdr.what(filepath)
if file_type not in ['png', 'jpeg', 'gif']:
os.remove(filepath)
abort(403, "Invalid image file")
Even if an attacker renames HTML to .png, it gets rejected.
4. Force Download Instead of Execution
return send_from_directory("uploads", filename, as_attachment=True)
The browser now downloads the file instead of executing it.
This prevents stored XSS completely.
5. Disable Debug Mode
app.run(debug=False)
Debug mode in production = instant compromise.
Final Secure Upload Flow
The new process:
- receive upload
- sanitize filename
- randomize filename
- save file
- verify binary type
- reject fake images
- safely serve file
Now the server treats uploads as untrusted input, not friendly data.
What I Learned Today
I started with a basic feature:
upload profile picture
I ended up learning:
- Stored XSS
- Path traversal
- File signature validation
- Secure file handling
- Debug mode RCE risk
- Why trusting user input breaks systems
Real vulnerabilities are rarely complicated.
They usually begin with something simple that developers assume is safe.
And file uploads… are never simple.





Top comments (0)