Unggah berkas yang aman di aplikasi web membutuhkan izin ketat, batas ukuran, Signed URL yang singkat, dan pola pemindaian sederhana untuk menghindari insiden.

Unggahan berkas terlihat tidak berbahaya: foto profil, PDF, spreadsheet. Tapi mereka sering menjadi insiden keamanan pertama karena memungkinkan orang asing mengirim kotak misteri ke sistem Anda. Jika Anda menerimanya, menyimpannya, dan menampilkannya kembali ke orang lain, Anda membuat jalur serangan baru ke aplikasi.
Risikonya bukan hanya “seseorang mengunggah virus.” Unggahan berbahaya bisa membocorkan file pribadi, membengkakkan tagihan penyimpanan, atau menipu pengguna agar memberikan akses. Berkas bernama “invoice.pdf” mungkin bukan PDF sama sekali. Bahkan PDF dan gambar yang asli bisa bermasalah jika aplikasi Anda mempercayai metadata, membuat preview otomatis, atau menyajikannya dengan aturan yang salah.
Kegagalan nyata biasanya seperti ini:
Satu detail yang memicu banyak insiden: menyimpan berkas tidak sama dengan menyajikan berkas. Penyimpanan adalah tempat Anda menyimpan byte. Penyajian adalah cara byte itu dikirim ke browser dan aplikasi. Hal yang salah terjadi ketika aplikasi menyajikan unggahan pengguna dengan tingkat kepercayaan dan aturan yang sama seperti situs utama, sehingga browser memperlakukan unggahan sebagai “terpercaya.”
“Cukup aman” untuk aplikasi kecil atau yang berkembang biasanya berarti Anda bisa menjawab empat pertanyaan tanpa mengada-ada: siapa yang bisa mengunggah, apa yang Anda terima, seberapa besar dan seberapa sering, dan siapa yang bisa membacanya nanti. Bahkan jika Anda membangun cepat (dengan kode yang digenerasi atau platform berbasis chat), pembatas itu tetap penting.
Anggap setiap unggahan sebagai input yang tidak dipercaya. Cara praktis menjaga unggahan aman adalah membayangkan siapa yang mungkin menyalahgunakannya dan seperti apa “keberhasilan” bagi mereka.
Kebanyakan penyerang adalah bot yang memindai form unggah yang lemah atau pengguna nyata yang mendorong batas untuk mendapatkan storage gratis, mengikis data, atau mengganggu layanan Anda. Kadang juga pesaing yang menguji kebocoran atau gangguan.
Apa yang mereka inginkan? Biasanya salah satu hasil ini:
Lalu petakan titik lemah. Endpoint unggah adalah pintu depan (file terlalu besar, format aneh, tingkat permintaan tinggi). Penyimpanan adalah ruang belakang (bucket publik, izin salah, folder bersama). URL unduh adalah jalur keluar (terprediksi, berlaku lama, atau tidak terkait ke pengguna).
Contoh: fitur “unggah resume”. Bot mengunggah ribuan PDF besar untuk menaikkan biaya, sementara pengguna yang menyalahgunakan mengunggah file HTML dan membagikannya sebagai “dokumen” untuk menipu orang lain.
Sebelum Anda menambahkan kontrol, putuskan apa yang paling penting untuk aplikasi Anda: privasi (siapa yang dapat membaca), ketersediaan (bisakah Anda terus menyajikan), biaya (penyimpanan dan bandwidth), dan kepatuhan (di mana data disimpan dan berapa lama disimpan). Daftar prioritas itu menjaga keputusan tetap konsisten.
Kebanyakan insiden unggahan bukanlah hack yang rumit. Mereka adalah bug sederhana “saya bisa melihat berkas orang lain.” Anggap izin sebagai bagian dari unggahan, bukan fitur yang Anda tambal di kemudian hari.
Mulailah dengan satu aturan: default deny. Anggap setiap objek yang diunggah privat sampai Anda secara eksplisit mengizinkan akses. “Privat secara default” adalah baseline yang kuat untuk invoice, berkas medis, dokumen akun, dan apa pun yang terkait ke pengguna. Buat file publik hanya ketika pengguna benar-benar mengharapkannya (seperti avatar publik), dan bahkan saat itu pertimbangkan akses bertempo.
Sederhanakan peran dan pisahkan tanggung jawab. Pembagian umum adalah:
Jangan mengandalkan aturan tingkat folder seperti “apa pun di /user-uploads/ aman.” Periksa kepemilikan atau akses tenant saat baca, untuk setiap berkas. Itu melindungi ketika seseorang pindah tim, keluar dari organisasi, atau berkas dipindah.
Pola support yang baik bersifat sempit dan sementara: berikan akses ke satu berkas tertentu, catat, dan kedaluwarsa otomatis.
Sebagian besar serangan unggahan dimulai dengan trik sederhana: berkas yang terlihat aman karena nama atau header browser, tetapi sebenarnya berbeda. Anggap semua yang dikirim klien tidak dipercaya.
Mulailah dengan allowlist: tentukan format exact yang Anda terima (misalnya .jpg, .png, .pdf) dan tolak sisanya. Hindari “semua gambar” atau “semua dokumen” kecuali Anda benar-benar membutuhkannya.
Jangan percaya ekstensi nama file atau header Content-Type dari klien. Keduanya mudah dipalsukan. Berkas bernama invoice.pdf bisa saja executable, dan Content-Type: image/png bisa bohong.
Pendekatan yang lebih kuat adalah memeriksa byte pertama berkas, sering disebut “magic bytes” atau signature berkas. Banyak format umum punya header konsisten (seperti PNG dan JPEG). Jika header tidak cocok dengan yang Anda izinkan, tolak.
Pengaturan validasi praktis:
Penggantian nama lebih penting daripada kedengarannya. Jika Anda menyimpan nama yang diberikan pengguna secara langsung, Anda mengundang trik path, karakter aneh, dan overwrite yang tidak disengaja. Gunakan ID yang digenerasi untuk penyimpanan dan simpan nama asli hanya untuk tampilan.
Untuk foto profil, terima hanya JPEG dan PNG, verifikasi header, dan hapus metadata jika Anda bisa. Untuk dokumen, pertimbangkan membatasi ke PDF dan menolak apa pun yang memiliki konten aktif. Jika nanti Anda memutuskan membutuhkan SVG atau HTML, perlakukan mereka sebagai potensi eksekusi dan isolasi.
Kebanyakan outage unggahan bukanlah “trik hacker.” Mereka adalah berkas besar, terlalu banyak permintaan, atau koneksi lambat yang mengikat server sampai aplikasi terasa down. Anggap setiap byte sebagai biaya.
Pilih ukuran maksimum per fitur, bukan satu angka global. Avatar tidak perlu batas yang sama dengan dokumen pajak atau video singkat. Tetapkan batas sekecil yang masih terasa normal, lalu tambahkan jalur “unggah besar” terpisah hanya saat benar-benar perlu.
Terapkan batas di lebih dari satu tempat, karena klien bisa berbohong: di logika aplikasi, pada web server atau reverse proxy, dengan timeout unggah, dan dengan penolakan awal ketika ukuran yang dideklarasikan terlalu besar (sebelum membaca seluruh body).
Contoh konkret: avatar dibatasi 2 MB, PDF dibatasi 20 MB, dan apa pun yang lebih besar memerlukan alur berbeda (seperti direct-to-object-storage dengan signed URL).
Bahkan berkas kecil bisa menjadi DoS jika seseorang mengunggahnya berulang-ulang. Tambahkan rate limit pada endpoint unggah per pengguna dan per IP. Pertimbangkan batas lebih ketat untuk traffic anonim daripada pengguna yang masuk.
Unggahan yang dapat dilanjutkan membantu pengguna nyata di jaringan buruk, tapi token sesi harus ketat: masa berlaku singkat, terkait ke pengguna, dan terikat pada ukuran dan tujuan berkas tertentu. Jika tidak, endpoint “resume” menjadi pipa gratis ke storage Anda.
Saat Anda memblokir unggahan, kembalikan error yang jelas ke pengguna (file terlalu besar, terlalu banyak permintaan) tetapi jangan bocorkan internals (stack trace, nama bucket, detail vendor).
Unggahan aman bukan hanya tentang apa yang Anda terima. Juga tentang ke mana berkas pergi dan bagaimana Anda mengembalikannya nanti.
Jauhkan byte unggahan dari database utama Anda. Kebanyakan aplikasi hanya perlu metadata di DB (owner user ID, nama file asli, tipe terdeteksi, ukuran, checksum, storage key, waktu dibuat). Simpan byte di object storage atau layanan file yang dibuat untuk blob besar.
Pisahkan file publik dan privat di level storage. Gunakan bucket atau container berbeda dengan aturan berbeda. File publik (seperti avatar publik) bisa dibaca tanpa login. File privat (kontrak, invoice, dokumen medis) tidak boleh pernah dapat dibaca publik, bahkan jika seseorang menebak URL.
Hindari menyajikan file pengguna dari domain yang sama dengan aplikasi Anda bila memungkinkan. Jika berkas berisiko lolos (HTML, SVG dengan skrip, atau anomali MIME sniffing browser), men-hostingnya di domain utama bisa mengubahnya menjadi pengambilalihan akun. Domain unduh atau domain storage terpisah membatasi radius kerusakan.
Saat mengunduh, paksa header yang aman. Atur Content-Type yang dapat diprediksi berdasarkan apa yang Anda izinkan, bukan klaim pengguna. Untuk apa pun yang bisa ditafsirkan browser, lebih baik mengirimnya sebagai unduhan.
Beberapa default yang mencegah kejutan:
Content-Disposition: attachment untuk dokumen.Content-Type yang aman (atau application/octet-stream).Retensi juga bagian dari keamanan. Hapus unggahan yang ditinggalkan, hapus versi lama setelah diganti, dan tetapkan batas waktu untuk file sementara. Data yang disimpan lebih sedikit berarti lebih sedikit yang bisa bocor.
Signed URL (sering disebut pre-signed URL) adalah cara umum untuk membiarkan pengguna mengunggah atau mengunduh file tanpa membuat bucket storage publik, dan tanpa mengirim setiap byte lewat API Anda. URL membawa izin sementara, lalu kedaluwarsa.
Dua alur umum:
Direct-to-storage mengurangi beban API, tapi membuat aturan storage dan batasan URL lebih penting.
Anggap signed URL seperti kunci sekali pakai. Buat spesifik dan kedaluwarsa cepat.
Pola praktis adalah membuat rekaman unggahan dulu (status: pending), lalu mengeluarkan signed URL. Setelah unggahan, konfirmasi objek ada dan cocok ukuran serta tipenya sebelum menandai siap.
Alur unggah aman kebanyakan adalah aturan jelas dan status yang jelas. Anggap setiap unggahan tidak terpercaya sampai semua pemeriksaan selesai.
Tuliskan apa yang diizinkan tiap fitur. Foto profil dan dokumen pajak sebaiknya tidak berbagi tipe berkas, batas ukuran, atau visibilitas yang sama.
Tetapkan tipe yang diizinkan dan batas ukuran per-fitur (misalnya: foto hingga 5 MB; PDF hingga 20 MB). Terapkan aturan yang sama di backend.
Buat “rekaman unggahan” sebelum byte tiba. Simpan: pemilik (user atau org), tujuan (avatar, invoice, attachment), nama file asli, ukuran maks yang diharapkan, dan status seperti pending.
Unggah ke lokasi privat. Jangan biarkan klien memilih path akhir.
Validasi lagi di server: ukuran, magic bytes/tipe, allowlist. Jika lolos, ubah status menjadi uploaded.
Pindai untuk malware dan ubah status menjadi clean atau quarantined. Jika pemindaian asinkron, kunci akses sambil menunggu.
Izinkan unduh, preview, atau pemrosesan hanya ketika status clean.
Contoh kecil: untuk foto profil, buat rekaman terkait user dan tujuan avatar, simpan secara privat, konfirmasi itu benar JPEG/PNG (bukan hanya bernama seperti itu), pindai, lalu hasilkan URL preview.
Pemindaian malware adalah jaring pengaman, bukan janji. Ia menangkap berkas berbahaya yang dikenal dan trik jelas, tapi tidak akan mendeteksi semuanya. Tujuan: kurangi risiko dan buat berkas yang tidak dikenal tidak berbahaya secara default.
Pola andal adalah karantina dulu. Simpan setiap unggahan baru ke lokasi privat dan tandai sebagai pending. Hanya setelah lolos pemeriksaan pindahkan ke lokasi “clean” (atau tandai tersedia).
Pemindaian sinkron hanya bekerja untuk berkas kecil dan traffic rendah karena pengguna menunggu. Kebanyakan aplikasi memindai secara asinkron: terima unggahan, kembalikan status “memproses”, pindai di background.
Pemindaian dasar biasanya engine antivirus (atau layanan) plus beberapa pengaman: pemindaian AV, pemeriksaan tipe berkas (magic bytes), batas pada arsip (zip bomb, zip bersarang, ukuran terkompresi sangat besar), dan pemblokiran format yang tidak perlu.
Jika scanner gagal, timeout, atau mengembalikan “unknown,” anggap berkas mencurigakan. Karantina dan jangan berikan link unduh. Di sinilah tim sering rugi: “scan failed” tidak boleh berubah menjadi “kirim saja.”
Saat Anda memblokir berkas, jaga pesan tetap netral: “Kami tidak dapat menerima berkas ini. Coba berkas lain atau hubungi dukungan.” Jangan klaim mendeteksi malware kecuali Anda yakin.
Pertimbangkan dua fitur: foto profil (ditampilkan publik) dan kwitansi PDF (privat, dipakai untuk tagihan atau dukungan). Keduanya adalah masalah unggahan, tetapi tidak seharusnya berbagi aturan yang sama.
Untuk foto profil, tetap ketat: izinkan hanya JPEG/PNG, batasi ukuran (mis. 2–5 MB), dan lakukan re-encode di server sehingga Anda tidak menyajikan byte asli pengguna. Simpan di storage publik hanya setelah pemeriksaan.
Untuk kwitansi PDF, terima ukuran lebih besar (mis. hingga 20 MB), tetapkan privat secara default, dan hindari merendernya inline dari domain utama aplikasi.
Model status sederhana menjaga pengguna terinformasi tanpa mengekspos internals:
Signed URL cocok di sini: gunakan signed URL singkat untuk unggah (write-only, satu object key). Terbitkan signed URL baca terpisah yang singkat, dan hanya saat status sudah clean.
Catat apa yang Anda butuhkan untuk investigasi, bukan isi berkas: user ID, file ID, tebakan tipe, ukuran, storage key, timestamp, hasil scan, ID request. Hindari mencatat isi mentah atau data sensitif yang ada di dalam dokumen.
Kebanyakan bug unggahan terjadi karena shortcut “sementara” kecil menjadi permanen. Anggap setiap berkas tidak dipercaya, setiap URL akan dibagikan, dan setiap pengaturan “akan kami perbaiki nanti” akan terlupakan.
Jebakan yang sering muncul:
Content-Type yang salah, membiarkan browser menafsirkan konten berisiko.Monitoring adalah hal yang sering di-skip sampai tagihan storage melonjak. Pantau volume unggahan, ukuran rata-rata, top uploader, dan tingkat error. Satu akun yang dikompromikan bisa diam-diam mengunggah ribuan berkas besar dalam semalam.
Contoh: sebuah tim menyimpan avatar dengan nama file yang diberikan pengguna seperti “avatar.png” di folder bersama. Satu pengguna menimpa gambar orang lain. Perbaikan yang membosankan tapi efektif: buat object key di server, simpan unggahan privat secara default, dan buka gambar yang di-resize melalui respons yang terkontrol.
Gunakan ini sebagai pemeriksaan akhir sebelum Anda rilis. Anggap setiap item sebagai pemblokir rilis, karena sebagian besar insiden datang dari satu pengaman yang hilang.
Content-Type yang dapat diprediksi, nama file aman, dan attachment untuk dokumen.Tuliskan aturan Anda dalam bahasa sederhana: tipe yang diizinkan, ukuran maksimal, siapa yang bisa mengakses apa, berapa lama signed URL hidup, dan apa arti “scan passed”. Itu menjadi kontrak bersama antara produk, engineering, dan support.
Tambahkan beberapa tes yang menangkap kegagalan umum: berkas oversized, executable yang diganti nama, baca tidak berwenang, signed URL kadaluarsa, dan unduh saat “scan pending”. Tes ini murah dibandingkan insiden.
Jika Anda membangun dan iterasi cepat, gunakan workflow yang memungkinkan merencanakan perubahan dan rollback dengan aman. Tim yang memakai Koder.ai (koder.ai) sering mengandalkan mode perencanaan dan snapshot/rollback saat mengetatkan aturan unggah dari waktu ke waktu, tetapi kebutuhan inti tetap sama: backend menegakkan kebijakan, bukan UI.
Mulailah dengan privat secara default dan anggap setiap unggahan sebagai input yang tidak dipercaya. Terapkan empat dasar ini di sisi server:
Jika Anda bisa menjawab itu dengan jelas, Anda sudah lebih aman daripada kebanyakan kasus insiden.
Karena pengguna bisa mengunggah sebuah “kotak misteri” yang aplikasi Anda simpan dan mungkin nanti disajikan ke pengguna lain. Itu bisa menyebabkan:
Jarang sekadar “seseorang mengunggah virus.”
Menyimpan berarti menaruh byte di suatu tempat. Menyajikan berarti mengirimkan byte itu ke browser dan aplikasi.
Bahaya muncul ketika aplikasi Anda menyajikan unggahan pengguna dengan tingkat kepercayaan dan aturan yang sama seperti situs utama. Jika berkas berisiko diperlakukan seperti halaman normal, browser bisa mengeksekusinya (atau pengguna bisa terlalu percaya padanya).
Default yang lebih aman: simpan secara privat, lalu sajikan melalui respons unduh yang terkontrol dengan header aman.
Gunakan default deny dan periksa akses setiap kali berkas diunduh atau dipreview.
Aturan praktis:
Jangan percaya ekstensi nama file atau Content-Type dari browser. Validasi di server:
Karena gangguan biasanya berasal dari penyalahgunaan yang membosankan: terlalu banyak unggahan, berkas sangat besar, atau koneksi lambat yang mengikat sumber daya.
Default yang efektif:
Anggap setiap byte sebagai biaya dan setiap permintaan sebagai potensi penyalahgunaan.
Ya, tapi lakukan dengan hati-hati. Signed URL membiarkan browser mengunggah/mengunduh langsung ke storage tanpa membuat bucket publik.
Default aman:
Direct-to-storage mengurangi beban API, tapi scoping dan expiry tidak bisa dinegosiasi.
Polanya paling aman adalah:
pendingPemindaian membantu, tapi bukan jaminan. Gunakan sebagai jaring pengaman, bukan satu-satunya kontrol.
Pendekatan praktis:
Kuncinya adalah kebijakan: “belum dipindai” tidak boleh berarti “tersedia.”
Sajikan berkas sedemikian rupa agar browser tidak menafsirkannya sebagai halaman web.
Default yang baik:
Content-Disposition: attachment untuk dokumenSebagian besar bug nyata adalah kesalahan sederhana “saya bisa melihat berkas pengguna lain.”
Jika byte tidak cocok dengan format yang diizinkan, tolak unggahan.
cleanquarantinedcleanIni mencegah berkas “scan gagal” atau “sedang diproses” dibagikan secara tidak sengaja.
Content-Type aman yang dipilih server (atau application/octet-stream)Ini mengurangi risiko berkas unggahan berubah menjadi halaman phishing atau mengeksekusi skrip.