Validasi upload file yang tahan abuse berarti menganggap setiap file dari pengguna sebagai input tidak tepercaya. Kontrol yang benar-benar berguna bukan sekadar memeriksa ekstensi, tetapi memvalidasi tipe file berdasarkan isi, membatasi ukuran dan jumlah upload, menyimpan file di lokasi terisolasi, serta mengatur bagaimana file diakses kembali.
Kalau tujuan Anda adalah mencegah remote code execution (RCE), XSS melalui file, path traversal, ZIP bomb, dan penyalahgunaan storage, maka pendekatannya harus berlapis. Tidak ada satu validasi tunggal yang cukup. Yang efektif adalah kombinasi allowlist tipe file, pemeriksaan magic bytes, penamaan file aman, karantina sebelum dipublikasikan, scanning, pembatasan parser, dan rate limiting per user/IP.
Threat model upload file: apa yang sebenarnya diserang?
Banyak implementasi upload gagal karena ancamannya dianggap terlalu sederhana: “asal file bisa diunggah dan disimpan”. Padahal permukaan serangnya luas. Beberapa skenario yang umum:
- RCE: penyerang mengunggah file yang dapat dieksekusi server, misalnya skrip yang tersimpan di direktori yang juga dilayani sebagai konten executable.
- XSS file: file HTML, SVG, atau dokumen tertentu dibuka langsung dari origin aplikasi dan mengeksekusi JavaScript di browser pengguna.
- Path traversal: nama file seperti
../../app/configmencoba menulis di lokasi yang tidak semestinya. - ZIP bomb / decompression bomb: file kecil yang ketika diekstrak menghabiskan CPU, RAM, atau disk secara ekstrem.
- Parser exploit: file gambar atau dokumen yang memicu bug pada library pemrosesan file.
- Abuse storage: bot mengunggah file besar atau banyak file untuk menghabiskan storage dan bandwidth.
- Konten berbahaya: malware, dokumen phishing, atau payload lain yang kemudian diunduh pengguna lain.
Dari threat model itu, prinsip desainnya jelas: jangan percaya metadata dari klien, jangan eksekusi file upload, jangan langsung publikasikan, dan jangan proses file tanpa batasan sumber daya.
Kontrol inti yang benar-benar berguna
1. Gunakan allowlist tipe file, bukan blacklist ekstensi
Mulailah dari daftar tipe file yang memang dibutuhkan aplikasi. Jika fitur hanya menerima avatar, Anda mungkin cukup menerima JPEG dan PNG. Jika fitur menerima lampiran dokumen, tentukan tipe yang jelas dan ukur risikonya.
Hindari blacklist seperti “tolak .php, .exe, .js”. Pendekatan ini rapuh karena format dan trik bypass terlalu banyak: ekstensi ganda, huruf besar-kecil, spasi, karakter Unicode, atau tipe file lain yang tetap berbahaya di konteks tertentu.
Semakin sempit allowlist, semakin kecil permukaan serang. Banyak aplikasi menerima terlalu banyak format tanpa alasan bisnis yang jelas.
2. MIME type, ekstensi, dan magic bytes: perannya berbeda
Tiga hal ini sering disamakan, padahal fungsinya berbeda:
- Ekstensi file berguna untuk UX dan pemetaan tipe umum, tetapi mudah dipalsukan.
- MIME type dari request biasanya berasal dari klien dan tidak boleh dipercaya sebagai sumber kebenaran.
- Magic bytes / signature dibaca dari isi file dan jauh lebih kuat untuk identifikasi tipe sebenarnya.
Pendekatan praktisnya:
- Normalisasi nama file dan ambil ekstensi hanya sebagai sinyal tambahan.
- Baca sebagian header file untuk mendeteksi signature format.
- Cocokkan hasil deteksi dengan allowlist.
- Jika perlu, pastikan ekstensi juga konsisten dengan tipe yang terdeteksi.
Contoh kebijakan sederhana:
- JPEG: izinkan hanya jika signature cocok dengan JPEG, ukuran masuk batas, dan parser gambar berhasil membaca dimensi.
- PNG: izinkan hanya jika signature cocok dengan PNG dan file tidak mengandung konten tak lazim untuk alur Anda.
- PDF: izinkan jika signature cocok dan file tidak diproses lebih jauh tanpa sandbox.
Untuk format yang kompleks seperti SVG, DOCX, atau file arsip, risiko lebih tinggi karena mereka bisa membawa skrip, referensi eksternal, atau struktur internal yang kompleks. Jika tidak benar-benar dibutuhkan, lebih aman tidak menerimanya.
// Pseudocode generik validasi tipe file
function validateType(fileStream, originalName):
ext = lower(getExtension(originalName))
detected = detectByMagicBytes(fileStream.peek(8192))
allowed = {
"jpg": ["image/jpeg"],
"jpeg": ["image/jpeg"],
"png": ["image/png"],
"pdf": ["application/pdf"]
}
if detected not in flatten(values(allowed)):
reject("Tipe file tidak diizinkan")
if ext exists and detected not in allowed[ext]:
reject("Ekstensi tidak cocok dengan isi file")
return detectedMengapa ini efektif? Karena penyerang bisa mengubah nama file dan header HTTP dengan mudah, tetapi lebih sulit memalsukan struktur dasar format file tanpa membuat file rusak atau terdeteksi tidak konsisten.
3. Batasi ukuran file, jumlah file, dan laju upload
Pembatasan ukuran adalah kontrol dasar, tetapi implementasinya harus ada di beberapa lapisan:
- Client-side untuk UX, agar pengguna tahu file terlalu besar sebelum upload.
- Reverse proxy / load balancer untuk memotong request besar lebih awal.
- Backend application sebagai sumber kebijakan final.
- Storage quota per user, tenant, atau organisasi.
Selain single file size limit, tambahkan juga:
- batas jumlah file per request,
- batas total ukuran per request,
- batas total storage per user/tenant,
- batas frekuensi upload per user/IP.
Tanpa quota dan rate limit, penyerang tidak perlu mencari celah teknis. Cukup mengunggah file valid dalam jumlah besar untuk menimbulkan biaya storage dan bandwidth.
4. Nama file aman: jangan pakai nama asli sebagai path
Nama file dari pengguna sebaiknya dianggap data presentasi saja, bukan identifier penyimpanan. Risiko utamanya:
- Path traversal jika nama file dipakai langsung dalam path.
- Tabrakan nama antar pengguna.
- Karakter aneh yang mengganggu filesystem, logging, header download, atau UI.
- XSS tersimpan bila nama file ditampilkan kembali tanpa encoding yang benar.
Praktik yang aman:
- buat identifier acak atau berbasis UUID untuk nama file internal,
- simpan nama asli sebagai metadata terpisah bila memang dibutuhkan,
- normalisasi dan sanitasi nama asli sebelum ditampilkan,
- jangan pernah menggabungkan path penyimpanan dari input mentah pengguna.
// Pseudocode generik penamaan aman
internalId = generateRandomId()
safeExt = mapDetectedMimeToExtension(detectedMime)
storageKey = "uploads/quarantine/" + internalId + "." + safeExt
saveMetadata({
id: internalId,
original_name: sanitizeForDisplay(originalName),
mime: detectedMime,
size: fileSize,
status: "quarantine"
})5. Simpan di lokasi terisolasi, bukan di document root yang executable
Ini salah satu kontrol paling penting untuk mencegah RCE. File upload sebaiknya:
- disimpan di bucket/object storage atau direktori non-public,
- tidak dapat dieksekusi sebagai skrip oleh web server,
- tidak langsung tersedia lewat URL permanen publik,
- diakses melalui lapisan aplikasi atau URL bertanda tangan dengan masa berlaku pendek.
Kesalahan klasik adalah menyimpan file upload di direktori yang dilayani langsung oleh web server dengan konfigurasi default. Jika ada salah konfigurasi tipe handler atau file berbahaya lolos, dampaknya bisa fatal.
Kalau file memang perlu diunduh publik, lebih aman gunakan salah satu pola ini:
- Private storage + aplikasi sebagai gatekeeper: aplikasi memeriksa otorisasi lalu melakukan streaming file.
- Private object storage + signed URL sementara: aplikasi menerbitkan URL sementara untuk file yang sudah lolos validasi dan aman dipublikasikan.
6. Scan malware, tetapi jangan menganggap scan sebagai satu-satunya benteng
Scanning malware berguna terutama untuk aplikasi yang menerima dokumen dari banyak pihak dan file akan diunduh ulang oleh pengguna lain. Namun scanning bukan pengganti validasi tipe, isolasi storage, atau kontrol akses.
Model yang umum:
- file diunggah ke area karantina,
- metadata disimpan dengan status
pending_scan, - worker asynchronous mengambil file dan menjalankan scanner,
- jika lolos, status menjadi
readydan file dipindahkan atau ditandai aman, - jika gagal, file dihapus atau dikarantina lebih lanjut.
Trade-off-nya:
- Sinkron: hasil langsung diketahui, tetapi latensi request naik.
- Asinkron: lebih skalabel, tetapi Anda harus mendesain status file dan perilaku UI/API selama file belum tersedia.
Untuk banyak aplikasi modern, pendekatan asinkron lebih praktis selama file tidak dipublikasikan sebelum status aman.
7. Batasi parser gambar dan proses transformasi
Gambar sering dianggap aman, padahal parser gambar juga bisa menjadi target. Jika aplikasi memproses gambar, misalnya membuat thumbnail atau membaca metadata, batasi perilakunya:
- gunakan library yang terpelihara dan minim fitur yang tidak diperlukan,
- batasi dimensi maksimum piksel, bukan hanya ukuran file,
- tolak file dengan resolusi ekstrem walau byte-nya kecil,
- hindari membaca metadata berlebih jika tidak perlu,
- lakukan re-encode ke format aman bila memungkinkan.
Re-encode sering efektif untuk image pipeline: decode file sumber dengan parser tepercaya, lalu tulis ulang ke JPEG/PNG standar dan buang metadata yang tidak dibutuhkan. Ini membantu mengurangi muatan aneh atau struktur non-standar dalam file asli.
Ukuran file kecil tidak berarti aman. Gambar dengan dimensi sangat besar bisa tetap menghabiskan memori saat didekode.
Alur backend yang disarankan
Berikut contoh alur backend generik lintas stack yang realistis untuk upload file modern:
- Autentikasi dan otorisasi
Pastikan endpoint upload hanya bisa dipakai aktor yang tepat. Terapkan kebijakan berbeda per role atau tenant bila perlu. - Rate limit dan quota check
Periksa batas per IP, per user, dan quota storage sebelum membaca seluruh file. - Streaming upload ke penyimpanan sementara
Hindari memuat seluruh file ke memori. Simpan ke area karantina atau temporary storage. - Validasi ukuran aktual
Bandingkan ukuran yang benar-benar diterima dengan kebijakan sistem. - Deteksi magic bytes
Identifikasi tipe file berdasarkan isi, bukan header HTTP. - Validasi ekstensi dan kebijakan tipe
Pastikan cocok dengan allowlist. - Validasi konten spesifik format
Contoh: baca dimensi gambar, tolak arsip jika fitur tidak mendukung arsip. - Simpan metadata aman
Simpan nama asli yang sudah dinormalisasi, MIME terdeteksi, hash file, ukuran, owner, status. - Scan asynchronous
Antrikan job untuk malware scan atau proses lanjutan seperti thumbnail. - Publikasi terkendali
Baru setelah statusready, file dapat diakses melalui signed URL sementara atau endpoint download terotorisasi.
// Pseudocode generik alur upload
function handleUpload(request, actor):
enforceAuth(actor)
enforceRateLimit(actor.id, request.ip)
enforceQuota(actor.id)
tmpFile = streamToTemporaryStorage(request.file)
if tmpFile.size > MAX_SIZE:
delete(tmpFile)
reject("Ukuran file melebihi batas")
mime = detectByMagicBytes(tmpFile.head)
if mime not in ALLOWED_MIME:
delete(tmpFile)
reject("Tipe file tidak diizinkan")
if mime startsWith "image/":
meta = inspectImage(tmpFile)
if meta.width > MAX_WIDTH or meta.height > MAX_HEIGHT:
delete(tmpFile)
reject("Dimensi gambar terlalu besar")
fileId = generateRandomId()
storageKey = quarantineKey(fileId, extensionFor(mime))
move(tmpFile, storageKey)
saveFileRecord({
id: fileId,
owner_id: actor.id,
mime: mime,
size: tmpFile.size,
status: "pending_scan",
storage_key: storageKey,
sha256: hashFile(storageKey)
})
enqueue("scan_file", { file_id: fileId })
return accepted({ file_id: fileId, status: "pending_scan" })Lalu worker scanner:
function scanFileJob(fileId):
file = loadFileRecord(fileId)
if not file or file.status != "pending_scan":
return
result = malwareScan(file.storage_key)
if result == "clean":
markReady(fileId)
else:
markRejected(fileId, reason="malware_or_policy")
deleteStoredFile(file.storage_key)URL publik sementara dan pola akses file yang aman
Setelah file lolos validasi, jangan otomatis membuatnya tersedia lewat URL permanen yang mudah ditebak. Pola yang lebih aman:
Private storage + endpoint download
Aplikasi mengecek apakah pengguna berhak mengakses file, lalu melakukan streaming. Kelebihannya adalah kontrol akses tetap terpusat. Kekurangannya, aplikasi menanggung lebih banyak beban bandwidth.
Signed URL sementara
Aplikasi membuat URL dengan masa berlaku singkat ke object storage privat. Ini cocok untuk file besar dan arsitektur yang ingin mengurangi beban aplikasi. Pastikan URL:
- punya masa berlaku pendek,
- dibuat hanya untuk file dengan status aman,
- tidak dibagikan secara permanen di halaman publik jika akses seharusnya terbatas.
Untuk mencegah XSS file, pertimbangkan juga bagaimana file disajikan:
- untuk file yang harus diunduh, gunakan header yang mendorong browser mengunduh alih-alih merender inline,
- hindari melayani HTML/SVG yang berasal dari pengguna dari origin utama aplikasi,
- jika perlu pratinjau, lakukan melalui transformasi aman atau domain terpisah dengan kebijakan ketat.
Rate limit per user/IP: kontrol anti-abuse yang sering dilupakan
Validasi file tidak menyelesaikan masalah abuse operasional. Upload endpoint hampir selalu menarik bagi bot karena mahal secara bandwidth, CPU, dan storage. Karena itu, terapkan beberapa lapisan pembatasan:
- Per IP untuk menghambat abuse anonim atau serangan sederhana.
- Per user/account untuk mencegah akun valid menyalahgunakan fitur.
- Per tenant/organisasi jika aplikasi multi-tenant.
- Concurrency limit agar satu aktor tidak membuka terlalu banyak upload paralel.
- Quota harian/bulanan untuk mengendalikan biaya storage dan transfer.
Rate limit berbasis IP saja tidak cukup karena banyak pengguna sah bisa berbagi NAT, sedangkan penyerang bisa berganti IP. Sebaliknya, hanya berbasis user juga tidak cukup untuk endpoint pra-login. Kombinasi keduanya lebih realistis.
Selain menolak request berlebih, log juga sinyal abuse seperti:
- banyak upload gagal karena tipe file tidak diizinkan,
- ukuran file sering tepat di bawah batas maksimum,
- banyak file identik dari banyak akun,
- lonjakan request dari ASN atau wilayah tertentu yang tidak biasa bagi produk Anda.
Kesalahan umum yang sering terjadi
- Hanya memeriksa ekstensi
Ini bypass paling klasik dan paling sering ditemukan. - Mempercayai Content-Type dari browser
Header ini mudah dipalsukan dan tidak cukup untuk keputusan keamanan. - Menyimpan upload di folder publik yang executable
Kesalahan ini membuka jalan ke RCE saat ada file berbahaya yang lolos. - Menggunakan nama file asli sebagai path storage
Memicu traversal, collision, dan masalah encoding. - Langsung mempublikasikan file sebelum scan/validasi selesai
Begitu URL tersebar, rollback jauh lebih sulit. - Tidak membatasi dimensi gambar
Aplikasi aman dari file besar, tetapi tetap tumbang saat mendekode gambar raksasa. - Menerima arsip lalu mengekstraknya tanpa batasan
Ini membuka risiko ZIP bomb, path traversal di dalam arsip, dan ledakan jumlah file. - Tidak punya quota storage per user
Abuse valid tetapi mahal akan lolos tanpa perlu eksploitasi teknis. - Menampilkan nama file tanpa encoding output
Nama file bisa menjadi vektor XSS tersimpan di UI admin atau halaman daftar file.
Catatan khusus untuk ZIP, arsip, dan dokumen kompleks
Jika aplikasi tidak benar-benar perlu menerima file arsip, pilihan paling aman adalah menolaknya. Arsip menambah banyak masalah:
- rasio kompresi ekstrem,
- banyak file internal,
- path traversal saat ekstraksi,
- format campuran yang sulit dipindai konsisten,
- pemakaian CPU dan disk yang sulit diprediksi.
Kalau arsip memang wajib didukung, minimal:
- batasi ukuran file arsip dan total ukuran hasil ekstraksi,
- batasi jumlah file di dalam arsip,
- tolak path absolut atau path dengan
.., - tolak symlink dan entri spesial,
- ekstrak di sandbox/temporary area terisolasi,
- scan isi setelah ekstraksi jika alurnya menuntut itu.
Dokumen seperti PDF atau format office juga perlu kehati-hatian. Walau sering diperlukan bisnis, jangan asumsi mereka aman hanya karena umum. Pertimbangkan apakah file akan diunduh saja, dipratinjau, diindeks, atau diproses lebih lanjut. Semakin banyak pemrosesan server-side, semakin tinggi risiko parser dan kompleksitas hardening.
Debugging dan observability
Upload file yang aman sering gagal bukan karena validasinya terlalu ketat, tetapi karena observability-nya buruk. Agar mudah dioperasikan:
- catat alasan penolakan yang spesifik di log internal,
- bedakan error pengguna dari error sistem,
- simpan metadata penting: ukuran, MIME terdeteksi, ekstensi, hash, owner, status scan, sumber IP, dan waktu,
- buat metrik untuk rejection rate per alasan, ukuran rata-rata upload, scan latency, dan quota hit rate,
- beri status yang jelas di database:
uploaded,pending_scan,ready,rejected,deleted.
Untuk pengguna, error message sebaiknya membantu tanpa membocorkan detail sensitif. Misalnya: “Tipe file tidak didukung” lebih baik daripada menjelaskan seluruh kebijakan internal parser.
Checklist implementasi validasi upload file yang tahan abuse
- Gunakan allowlist tipe file yang benar-benar dibutuhkan.
- Validasi tipe berdasarkan magic bytes, bukan ekstensi atau header klien saja.
- Cocokkan ekstensi dengan tipe terdeteksi sebagai pemeriksaan tambahan.
- Batasi ukuran file, jumlah file per request, dan total ukuran per request.
- Terapkan quota storage per user/tenant dan rate limit per user/IP.
- Gunakan nama file internal acak; simpan nama asli hanya sebagai metadata.
- Jangan simpan upload di lokasi yang executable atau langsung public.
- Gunakan karantina dan status file sebelum file dinyatakan aman.
- Lakukan malware scan jika alur bisnis memungkinkan file dibagikan atau diunduh pihak lain.
- Batasi dimensi gambar dan pertimbangkan re-encode untuk image pipeline.
- Tolak atau sandbox arsip dan format kompleks bila tidak benar-benar diperlukan.
- Cegah path traversal pada nama file dan saat mengekstrak arsip.
- Hindari melayani file aktif seperti HTML/SVG dari origin utama tanpa kontrol tambahan.
- Gunakan URL publik sementara atau endpoint download terotorisasi.
- Catat metadata, alasan penolakan, dan status scan untuk audit dan debugging.
Penutup
Validasi upload file yang tahan abuse bukan soal menambah checklist sebanyak mungkin, melainkan memilih kontrol yang mengurangi risiko nyata pada jalur yang benar. Untuk sebagian besar aplikasi web modern, fondasi yang paling efektif adalah: allowlist format, deteksi isi file dengan magic bytes, batas ukuran dan quota, nama file aman, storage terisolasi, publikasi tertunda sampai lolos scan, pembatasan parser, dan rate limit per user/IP.
Jika Anda hanya memperbaiki satu hal minggu ini, jangan mulai dari regex nama file. Mulailah dari memindahkan file upload ke storage privat non-executable dan memvalidasi tipe berdasarkan isi file. Dua langkah itu biasanya memberi penurunan risiko yang jauh lebih besar daripada banyak validasi kosmetik.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!