DEV Community

Cover image for Day 6 — I Built a Profile Picture Upload… and turned My Website into a Malware Host
Hafiz Shamnad
Hafiz Shamnad

Posted on

Day 6 — I Built a Profile Picture Upload… and turned My Website into a Malware Host

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>"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

I was reading the file using:

open(filepath).read()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

I uploaded it.

Then opened:

http://127.0.0.1:5000/files/evil.html
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So I implemented real defenses.


1. Sanitize the Filename

Attackers can upload:

../../../../app.py
Enter fullscreen mode Exit fullscreen mode

I used:

from werkzeug.utils import secure_filename
safe_name = secure_filename(file.filename)
Enter fullscreen mode Exit fullscreen mode

This prevents path traversal and overwriting server files.


2. Randomize the Name

import uuid
filename = str(uuid.uuid4()) + "_" + safe_name
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The browser now downloads the file instead of executing it.

This prevents stored XSS completely.


5. Disable Debug Mode

app.run(debug=False)
Enter fullscreen mode Exit fullscreen mode

Debug mode in production = instant compromise.


Final Secure Upload Flow

The new process:

  1. receive upload
  2. sanitize filename
  3. randomize filename
  4. save file
  5. verify binary type
  6. reject fake images
  7. 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)