Fitur upload file sering terlihat sederhana di permukaan, tetapi di sistem produksi ia menyentuh banyak area kritis: keamanan, performa, biaya bandwidth, kontrol akses, dan integritas data. Pada aplikasi Nuxt 3, keputusan paling penting biasanya bukan sekadar bagaimana mengirim file, tetapi di mana file lewat, siapa yang berhak mengunggah, dan bagaimana memastikan file yang tersimpan tetap aman.
Artikel ini membahas dua pola utama upload file pada Nuxt 3: upload melalui server API dan direct upload ke object storage menggunakan signed URL. Kita juga akan membahas validasi tipe dan ukuran file, pembatasan akses, progress upload di sisi klien, penyimpanan metadata ke database, serta mitigasi risiko umum seperti malware upload, path traversal, dan penyalahgunaan endpoint.
Memahami Dua Arsitektur Upload
1. Upload melalui server API Nuxt
Pada pola ini, browser mengirim file ke endpoint server, misalnya /api/upload. Server Nuxt menerima file, memvalidasi isinya, lalu meneruskan atau menyimpannya ke S3/cloud storage.
Kelebihan:
- Kontrol penuh terhadap proses validasi dan transformasi.
- Lebih mudah menambahkan scanning, resize gambar, atau audit logging sebelum file masuk ke storage.
- Kredensial storage tidak pernah terekspos ke klien.
Kekurangan:
- Server aplikasi menjadi bottleneck bandwidth.
- Resource CPU, memory, dan network server meningkat drastis untuk file besar.
- Kurang efisien untuk trafik tinggi atau upload video/dokumen besar.
2. Direct upload dengan signed URL
Pada pola ini, klien meminta URL bertanda tangan ke server, lalu mengunggah file langsung ke S3 atau layanan setara. Server tidak menjadi jalur data utama file, melainkan hanya mengeluarkan izin upload yang dibatasi.
Kelebihan:
- Lebih scalable karena beban upload tidak melewati server aplikasi.
- Latensi dan biaya infrastruktur aplikasi lebih rendah.
- Cocok untuk file besar atau volume upload tinggi.
Kekurangan:
- Logika validasi harus dirancang hati-hati karena upload terjadi langsung dari browser.
- Perlu desain metadata dan status file yang jelas agar database tetap konsisten.
- Beberapa pemeriksaan lanjutan, seperti antivirus, biasanya harus dilakukan secara asynchronous setelah upload.
Secara umum, direct upload dengan signed URL adalah pilihan utama untuk sistem yang ingin scalable. Sementara itu, upload via server API tetap relevan ketika Anda perlu inspeksi penuh terhadap file sebelum file disimpan.
Alur Aman yang Direkomendasikan
Untuk banyak aplikasi modern, alur berikut adalah kompromi yang baik antara keamanan dan skalabilitas:
- User login dan meminta izin upload ke endpoint backend.
- Backend memvalidasi hak akses user, tipe file yang diizinkan, ukuran maksimum, dan konteks bisnisnya.
- Backend membuat record metadata awal di database dengan status
pending. - Backend menghasilkan signed URL atau presigned POST dengan batasan tertentu.
- Browser upload langsung ke S3/cloud storage.
- Setelah berhasil, klien memanggil endpoint konfirmasi atau backend menerima event dari storage.
- Backend mengubah status metadata menjadi
uploadedatauready. - Proses async seperti antivirus scan, thumbnail generation, atau content moderation berjalan di background.
Prinsip penting: jangan menganggap file aman hanya karena ekstensi atau header HTTP terlihat benar. Validasi harus berlapis, dan file sebaiknya diperlakukan sebagai data tidak tepercaya sampai lolos pemeriksaan.
Implementasi Direct Upload di Nuxt 3 dengan S3
Endpoint server untuk membuat signed URL
Di Nuxt 3, Anda dapat membuat endpoint server menggunakan Nitro. Endpoint ini bertugas memverifikasi user dan mengeluarkan signed URL yang terbatas untuk satu file tertentu.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
const { filename, contentType, size } = body
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
const maxSize = 5 * 1024 * 1024
if (!allowedTypes.includes(contentType)) {
throw createError({ statusCode: 400, statusMessage: 'Tipe file tidak diizinkan' })
}
if (size > maxSize) {
throw createError({ statusCode: 400, statusMessage: 'Ukuran file melebihi batas' })
}
const safeName = crypto.randomUUID()
const key = `uploads/${user.id}/${safeName}`
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType
})
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 60 })
return {
key,
uploadUrl,
maxSize,
contentType
}
})Beberapa hal penting dari contoh di atas:
- Jangan gunakan nama file asli sebagai path utama. Gunakan ID acak untuk mencegah collision dan mengurangi risiko traversal atau penamaan berbahaya.
- Batasi content type dan ukuran file di backend. Klien boleh melakukan validasi awal, tetapi keputusan final tetap di server.
- Gunakan masa berlaku signed URL yang singkat. Misalnya 1-5 menit.
Upload file dari komponen Nuxt 3
Di sisi klien, alurnya adalah meminta signed URL lalu melakukan upload langsung menggunakan fetch atau XMLHttpRequest. Untuk progress upload, XMLHttpRequest masih lebih praktis karena menyediakan event progress upload secara native.
const progress = ref(0)
async function uploadFile(file) {
const signed = await $fetch('/api/uploads/sign', {
method: 'POST',
body: {
filename: file.name,
contentType: file.type,
size: file.size
}
})
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('PUT', signed.uploadUrl)
xhr.setRequestHeader('Content-Type', file.type)
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
progress.value = Math.round((event.loaded / event.total) * 100)
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve()
else reject(new Error('Upload gagal'))
}
xhr.onerror = reject
xhr.send(file)
})
await $fetch('/api/uploads/complete', {
method: 'POST',
body: {
key: signed.key,
originalName: file.name,
contentType: file.type,
size: file.size
}
})
}Pola /complete berguna untuk menyimpan metadata final ke database atau menandai status file sebagai selesai diunggah. Ini penting karena upload ke storage dan penyimpanan data aplikasi adalah dua hal berbeda.
Validasi File: Jangan Hanya Percaya Ekstensi
Validasi di klien
Validasi di klien berguna untuk pengalaman pengguna yang lebih baik: menolak file terlalu besar lebih cepat, menampilkan pesan error jelas, dan mengurangi request yang tidak perlu. Namun, validasi klien tidak boleh dianggap aman karena bisa dilewati dengan mudah.
Validasi di server
Validasi yang benar sebaiknya mencakup:
- Ukuran file maksimum, baik per file maupun total per user.
- MIME type yang diizinkan.
- Magic number atau signature file jika file sempat melewati server atau diperiksa oleh worker.
- Aturan bisnis, misalnya hanya admin yang boleh upload PDF kontrak, atau hanya pengguna premium yang boleh upload video.
Kesalahan umum adalah memeriksa hanya file.name.endsWith('.png'). Ini tidak cukup. File berbahaya bisa diberi nama ekstensi yang tampak aman.
Pembatasan Akses dan Desain Bucket
Bucket object storage sebaiknya private by default. Jangan membuat semua file publik kecuali memang diperlukan. Akses download idealnya dilakukan dengan salah satu cara berikut:
- Signed URL untuk download, jika file hanya boleh diakses sementara.
- Proxy melalui backend, jika Anda butuh audit, authorization tambahan, atau transformasi respons.
- Bucket publik terbatas hanya untuk aset yang memang aman dipublikasikan, misalnya gambar profil yang sudah diproses.
Praktik baik lain:
- Pisahkan prefix atau bucket berdasarkan domain data, misalnya
uploads/private/danuploads/public/. - Gunakan IAM policy sesempit mungkin. Server pembuat signed URL tidak perlu akses penuh ke seluruh akun cloud.
- Jika mendukung multi-tenant, sertakan tenant ID dalam namespace object key untuk memudahkan isolasi data.
Penyimpanan Metadata di Database
File di object storage bukan pengganti metadata aplikasi. Simpan informasi penting di database, misalnya:
- ID file
- ID pemilik/user
- Object key di storage
- Nama file asli
- MIME type
- Ukuran file
- Status:
pending,uploaded,scanning,ready,rejected - Hash/checksum bila diperlukan
- Waktu upload dan waktu scan
Dengan metadata yang baik, Anda dapat menerapkan lifecycle yang rapi: file gagal scan bisa dihapus otomatis, file yatim tanpa relasi bisnis bisa dibersihkan terjadwal, dan sistem bisa menampilkan status yang akurat ke pengguna.
Mitigasi Risiko Keamanan
Malware upload
Jika aplikasi menerima dokumen dari pengguna, pertimbangkan pipeline scanning antivirus. Untuk arsitektur direct upload, scanning biasanya dilakukan oleh worker asynchronous yang dipicu event object-created dari S3 atau queue. Selama file belum lolos scan, tandai statusnya quarantine atau scanning dan jangan izinkan file didownload pengguna lain.
Path traversal
Pada object storage modern, path traversal tidak sama seperti filesystem lokal, tetapi tetap ada risiko bila Anda menggunakan nama file pengguna secara mentah untuk membentuk key. Hindari pola seperti uploads/${userInputFilename}. Gunakan ID acak dan simpan nama asli hanya sebagai metadata terpisah.
Abuse dan spam upload
Endpoint pembuat signed URL rawan disalahgunakan jika tidak dibatasi. Mitigasinya:
- Wajib autentikasi.
- Rate limiting per user atau per IP.
- Quota harian atau bulanan.
- Validasi ukuran maksimal sebelum signed URL dibuat.
- Masa berlaku signed URL yang singkat.
Content-Type spoofing
Klien bisa mengirim header Content-Type palsu. Karena itu, file penting sebaiknya diperiksa ulang setelah upload, terutama sebelum diproses atau dibagikan ke user lain.
File overwrite
Jangan izinkan user menentukan object key akhir secara bebas. Bila key bisa ditebak atau dipilih sendiri, ada risiko overwrite file milik user lain atau collision antar upload.
Kapan Memilih Upload via Server API?
Pilih upload melalui server jika:
- File harus diperiksa sebelum pernah masuk storage utama.
- Anda butuh transformasi sinkron, misalnya resize gambar langsung.
- Ukuran file relatif kecil dan trafik masih moderat.
- Infrastruktur sederhana lebih penting daripada skala maksimum.
Namun, pastikan server tidak memuat seluruh file ke memory. Gunakan streaming jika memungkinkan, terutama untuk file besar. Menampung seluruh multipart body di memory adalah sumber error yang umum pada deployment dengan memory limit ketat.
Cloud Storage Selain AWS S3
Pendekatan yang sama berlaku untuk layanan setara seperti Cloudflare R2, Google Cloud Storage, atau DigitalOcean Spaces. Konsep intinya tetap sama: backend menghasilkan kredensial atau signed URL jangka pendek, klien upload langsung, lalu aplikasi menyimpan metadata dan menjalankan pemeriksaan lanjutan.
Perbedaannya biasanya ada pada:
- Format signed URL atau signed policy.
- SDK yang digunakan.
- Eventing untuk trigger scanning atau post-processing.
- Model biaya egress, request, dan storage.
Debugging dan Kesalahan yang Sering Terjadi
- Upload 403 ke S3: biasanya karena signed URL kadaluarsa, header yang dikirim tidak cocok, atau IAM policy kurang tepat.
- CORS error di browser: bucket belum mengizinkan origin, method, atau header yang dibutuhkan.
- Metadata ada, file tidak ada: proses database dan upload tidak sinkron. Gunakan status
pendinglalu finalisasi setelah upload benar-benar sukses. - File sukses diupload tapi tidak bisa dibuka:
Content-Typesalah atau file korup saat dikirim. - Progress upload tidak muncul: Anda kemungkinan memakai
fetchtanpa mekanisme progress upload yang memadai. GunakanXMLHttpRequestatau library yang mendukung progress.
Rekomendasi Praktis
Untuk mayoritas aplikasi Nuxt 3, strategi yang aman dan scalable adalah:
- Gunakan direct upload dengan signed URL untuk file besar atau volume tinggi.
- Simpan bucket dalam mode private.
- Validasi autentikasi, tipe file, ukuran, dan quota sebelum signed URL diterbitkan.
- Gunakan object key acak, bukan nama file asli.
- Simpan metadata upload di database dengan status yang jelas.
- Tambahkan scanning asynchronous untuk file yang berisiko.
- Terapkan rate limiting dan audit logging pada endpoint upload.
Upload file yang aman bukan fitur tambahan, melainkan bagian penting dari desain sistem. Dengan memisahkan jalur data file dari logika otorisasi, memvalidasi input secara berlapis, dan menyimpan metadata yang konsisten, Anda bisa membangun fitur upload di Nuxt 3 yang tetap efisien saat trafik tumbuh tanpa mengorbankan keamanan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!