Test case 19 / 26FAIL

Unrestricted File Upload - Profile Picture - File Type Validation

Lab environment — not a production system

Screenshots and raw traffic are not translated; text within them may be in another language.

vuln-bank/Evidence/[FAIL] Unrestricted File Upload - Profile Picture - File Type Validation

01 — Notes

Test notes

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.
View source note (English)
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.

POC:
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.

02 — Screenshots

Screenshots (3)

01 / 1
02 / 2
03 / 3

03 — Requests and responses

HTTP captures

Some credential values are masked in this public HTML view. This does not mean every sensitive value or linked file has been redacted.

CAPTURE 01 / 1
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
\x1a
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
--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"
}
CAPTURE 02 / 2
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"
}
CAPTURE 03 / 3
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>