Portal distribusi mod perlu memperlakukan setiap arsip sebagai input tidak tepercaya. Dalam konteks komunitas yang membutuhkan kanal distribusi mod, termasuk setelah rilis OpenMW 0.51.0, endpoint upload tidak cukup hanya memeriksa ekstensi .zip lalu menyimpan berkas. Backend harus membatasi biaya komputasi dan penyimpanan, mencegah arsip menulis ke lokasi tak sah, serta memastikan mod tidak dapat dipublikasikan sebelum seluruh pemeriksaan selesai.
Hardening upload mod OpenMW yang efektif menggunakan alur berlapis: autentikasi dan otorisasi, rate limit, kuota, upload terbatas langsung ke object storage, status karantina, inspeksi arsip di worker terisolasi, pemindaian malware, dan publikasi atomik. Pendekatan ini membuat server API tetap ringan dan mencegah berkas berbahaya menjadi konten publik.
Threat model endpoint upload arsip
Mulailah dari aset yang perlu dilindungi: kapasitas object storage, CPU dan memori worker, filesystem internal, data pengguna lain, reputasi portal, serta pengguna yang mengunduh mod. Berikut ancaman yang perlu ditangani secara eksplisit.
- ZIP bomb atau decompression bomb. Arsip kecil dapat mengembang menjadi data sangat besar, menghabiskan disk, CPU, atau memori saat diekstrak.
- Zip slip/path traversal. Entri seperti
../../.ssh/authorized_keys,/etc/passwd, atau path Windows dapat keluar dari direktori ekstraksi. - Ekstensi dan MIME palsu. Nama
mod.zipatau headerContent-Type: application/zipbukan bukti isi berkas benar-benar arsip ZIP yang valid. - Berkas terlalu besar dan banyak entri. Bahkan tanpa compression ratio ekstrem, jutaan entri kecil dapat menyebabkan waktu scan dan operasi metadata yang mahal.
- Malware dan konten berbahaya. Arsip dapat berisi executable, skrip, dokumen berbahaya, atau payload yang memanfaatkan pemindai/ekstraktor rentan.
- Overwrite dan konflik nama. Penyimpanan berdasarkan nama asli pengguna berisiko menimpa mod atau versi milik pengguna lain.
- Spam akun. Akun baru, akun curian, atau bot dapat mengisi penyimpanan, antrean scanner, dan indeks pencarian.
Jangan mengekstrak arsip di proses HTTP request, di direktori aplikasi, atau dengan hak akses yang dapat menulis ke filesystem produksi. Pemeriksaan arsip adalah pekerjaan asynchronous dan harus dianggap sebagai eksekusi terhadap input berbahaya.
Arsitektur alur upload yang aman
Pisahkan jalur penerimaan upload dari jalur verifikasi. API hanya membuat sesi upload dan metadata awal; object storage menerima byte berkas; worker memeriksa berkas di lingkungan terbatas.
- Autentikasi dan otorisasi. Verifikasi identitas pengguna dan izin unggah. Terapkan kebijakan yang lebih ketat untuk akun baru, misalnya status verifikasi email dan batas upload lebih kecil sampai memiliki rekam jejak yang baik.
- Buat record upload. Simpan record database dengan ID acak/UUID, pemilik, nama file yang dinormalisasi untuk tampilan, ukuran yang diminta, tipe yang diizinkan, dan status
pending_upload. - Terbitkan URL upload sementara. Berikan presigned URL atau mekanisme upload langsung lain menuju prefix karantina, misalnya
quarantine/{upload_id}/source. Batasi ukuran danContent-Typepada policy storage bila penyedia mendukungnya. - Finalisasi upload. Klien memanggil endpoint finalisasi. Server mengambil ukuran dan checksum dari metadata object storage, tidak mempercayai ukuran atau hash yang dikirim klien.
- Masukkan pekerjaan scan ke antrean. Ubah status menjadi
queued_for_scandan buat job idempoten berdasarkanupload_idserta versi objek/checksum. - Inspeksi dalam worker terisolasi. Worker membaca arsip dari storage, melakukan validasi struktur, ekstraksi terbatas bila diperlukan, pemeriksaan manifest, dan pemindaian malware.
- Publikasi atomik. Hanya jika semua tahap lulus, pindahkan atau salin objek ke prefix publik/terverifikasi, buat record versi mod, lalu ubah status menjadi
published. Jika gagal, simpan alasan aman untuk pengguna dan objek tetap berada di karantina sampai masa retensi berakhir.
Status yang sebaiknya eksplisit
Status eksplisit mencegah frontend menganggap upload selesai sebagai mod yang siap diunduh. Contoh state machine: pending_upload → uploaded → queued_for_scan → scanning → approved → published. Jalur gagal dapat menuju rejected, failed_retryable, atau quarantined. Hanya status published yang boleh muncul dalam katalog dan memiliki URL unduh.
Kontrol operasional: batas ukuran, kuota, dan rate limit
Nilai berikut adalah contoh kebijakan awal, bukan angka universal. Sesuaikan dengan kapasitas storage, waktu proses worker, tipe arsip yang didukung, dan perilaku komunitas. Yang penting, semua batas diterapkan di lebih dari satu lapisan: API, policy object storage, dan worker.
| Kontrol | Contoh batas awal | Tujuan |
|---|---|---|
| Ukuran satu arsip terkompresi | 500 MiB | Membatasi biaya transfer dan penyimpanan per upload. |
| Total ukuran setelah ekstraksi | 2 GiB | Mengurangi dampak ZIP bomb dan konsumsi disk worker. |
| Rasio ekstraksi | Maksimum 20:1 | Menolak arsip kecil dengan payload hasil ekstraksi tidak proporsional. |
| Jumlah entri arsip | 10.000 | Mencegah serangan banyak berkas kecil dan metadata exhaustion. |
| Ukuran satu entri hasil ekstraksi | 512 MiB | Membatasi berkas tunggal yang tidak wajar. |
| Upload per akun terverifikasi | 10 arsip per 24 jam | Mengendalikan spam dan beban antrean. |
| Kuota akun | 5 GiB objek aktif dan karantina | Mencegah satu akun menghabiskan storage. |
| Rate limit endpoint sesi upload | 30 permintaan per jam per akun, dengan batas IP tambahan | Mengurangi otomatisasi dan pembuatan sesi berlebihan. |
Rate limit per akun adalah kontrol utama setelah autentikasi, tetapi tetap tambahkan limit per IP untuk menghadapi pembuatan akun otomatis dan serangan pada endpoint publik. Jangan menggunakan IP sebagai satu-satunya identitas: pengguna sah dapat berbagi NAT, sedangkan pelaku dapat berganti IP. Untuk akun baru, gunakan tier limit lebih rendah dan naikkan secara bertahap berdasarkan verifikasi atau persetujuan moderator.
Validasi arsip: signature, path, rasio, dan manifest
Ekstensi, MIME dari browser, dan nama berkas hanya metadata yang dikendalikan pengguna. Periksa signature/magic bytes pada object yang sudah diunggah dan gunakan parser arsip yang mendukung pembacaan metadata tanpa ekstraksi penuh. Bila hanya mendukung ZIP pada tahap awal, tolak format lain secara eksplisit; menambah format arsip berarti menambah parser, aturan path, dan permukaan serangan.
Normalisasi nama dan object key
Pisahkan nama tampilan dari nama penyimpanan. Nama asli boleh disimpan setelah validasi untuk ditampilkan di UI, tetapi object key harus dibentuk server menggunakan ID internal. Tolak karakter kontrol, path separator, nama kosong, dan nama yang terlalu panjang. Jangan pernah melakukan operasi filesystem berdasarkan nama dari pengguna.
display_name = normalize_unicode(client.filename)
display_name = basename(display_name)
assert display_name is not empty
assert no_control_characters(display_name)
assert byte_length(display_name) <= 180
upload_id = uuid_v4()
object_key = "quarantine/" + upload_id + "/source"
Normalisasi Unicode berguna untuk tampilan dan pencarian, tetapi jangan mengandalkannya untuk keamanan path. Keamanan path berasal dari object key yang dibuat server dan validasi setiap entri arsip secara terpisah.
Pseudocode validasi arsip
function inspectArchive(object, limits):
assert object.size <= limits.maxCompressedBytes
stream = object.openReadOnly()
assert hasAllowedArchiveSignature(stream)
archive = openArchiveMetadataOnly(stream)
totalUncompressed = 0
entryCount = 0
manifestCandidates = []
for entry in archive.entries():
entryCount += 1
if entryCount > limits.maxEntries:
reject("TOO_MANY_ENTRIES")
name = entry.name
if isAbsolutePath(name) or hasDrivePrefix(name):
reject("UNSAFE_ARCHIVE_PATH")
normalized = normalizeArchivePath(name)
if normalized == "" or normalized.startsWith("../") or containsNul(name):
reject("UNSAFE_ARCHIVE_PATH")
if entry.isSymlink() or entry.isHardlink() or entry.isDeviceFile():
reject("UNSUPPORTED_ARCHIVE_ENTRY")
if entry.uncompressedSize > limits.maxSingleEntryBytes:
reject("ENTRY_TOO_LARGE")
totalUncompressed += entry.uncompressedSize
if totalUncompressed > limits.maxTotalUncompressedBytes:
reject("ARCHIVE_EXPANDS_TOO_LARGE")
if isCandidateManifest(normalized):
manifestCandidates.append(normalized)
if hasForbiddenExtension(normalized):
reject("FORBIDDEN_CONTENT")
if object.size == 0 or totalUncompressed / object.size > limits.maxExpansionRatio:
reject("SUSPICIOUS_COMPRESSION_RATIO")
manifest = readManifestWithSizeLimit(archive, manifestCandidates)
validateManifestSchema(manifest)
validateManifestPathsExist(manifest, archive)
return { totalUncompressed, entryCount, manifest }
Implementasi nyata harus menangani ukuran yang tidak tersedia atau tidak tepercaya pada header arsip. Saat mengekstrak untuk pemindaian, hitung byte yang benar-benar dikeluarkan sambil menyalin stream dan hentikan proses ketika batas terlampaui. Jangan mengasumsikan metadata uncompressedSize selalu cukup untuk melindungi worker.
Validasi manifest bergantung pada format portal. Tetapkan skema manifest milik portal, misalnya identifier mod, versi, daftar file, dan metadata kompatibilitas. Batasi ukuran manifest, gunakan parser yang aman, tolak field tak dikenal bila skema perlu ketat, dan verifikasi bahwa setiap path yang dirujuk benar-benar ada di arsip. Manifest tidak boleh memberikan instruksi shell, URL yang otomatis diambil worker, atau path yang diakses di luar arsip.
Worker terisolasi dan pemindaian malware
Worker scanner memproses input yang paling berisiko. Jalankan di container, VM, atau sandbox dengan filesystem root read-only, direktori kerja sementara yang unik, user tanpa privilese, jaringan keluar yang diblokir secara default, serta batas CPU, memori, disk, jumlah proses, dan waktu eksekusi. Mount object storage secara terbatas atau unduh objek ke direktori sementara; jangan berikan kredensial storage dengan izin menulis ke prefix publik.
Ekstraksi harus dilakukan ke direktori baru yang dikelola worker. Setelah setiap entri diekstrak, lakukan pemeriksaan ulang bahwa path hasil resolusi masih berada di bawah root ekstraksi. Hindari fitur ekstraktor yang mengikuti symlink. Hapus direktori sementara dalam blok finally, termasuk saat job gagal atau dibatalkan.
Pemindaian malware merupakan lapisan tambahan, bukan pengganti validasi arsip. Kirim file yang sudah diekstrak secara terbatas atau arsip sumber ke engine antivirus yang dikelola organisasi. Catat versi definisi scanner dan hasilnya. Bila scanner tidak tersedia atau mengalami timeout, gunakan status failed_retryable atau quarantined; jangan fail-open dan menerbitkan mod tanpa scan yang diwajibkan kebijakan.
Format yang didukung dan executable
Tetapkan allowlist berdasarkan kebutuhan portal. Mod data OpenMW mungkin perlu berisi aset, plugin, dokumentasi, atau konfigurasi, tetapi portal tidak perlu menerima setiap tipe file. Memblokir executable native dan skrip yang dapat dijalankan langsung mengurangi risiko bagi pengguna, walaupun tidak menjamin seluruh isi aman. Jika komunitas memang membutuhkan tipe berisiko, buat jalur moderasi manual, label peringatan, dan kebijakan distribusi terpisah.
Kontrak API dan respons error
Gunakan kode error stabil yang dapat ditangani frontend, disertai pesan aman untuk pengguna. Jangan mengembalikan path internal, signature antivirus, detail sandbox, atau aturan deteksi yang dapat membantu pelaku menghindari kontrol.
POST /api/mods/uploads
{
"filename": "my-mod-1.2.zip",
"content_type": "application/zip",
"size_bytes": 73400320
}
201 Created
{
"upload_id": "b01ab5b6-4ca8-4f0c-8e78-2dc969739c51",
"status": "pending_upload",
"upload_url": "https://storage.example/...",
"expires_at": "2026-03-15T12:10:00Z"
}
429 Too Many Requests
{
"error": {
"code": "UPLOAD_RATE_LIMITED",
"message": "Batas upload sementara tercapai. Coba lagi setelah waktu yang ditentukan.",
"retry_after_seconds": 1800
}
}
422 Unprocessable Content
{
"error": {
"code": "UNSAFE_ARCHIVE_PATH",
"message": "Arsip berisi path file yang tidak diizinkan."
}
}
Endpoint finalisasi sebaiknya mengembalikan 202 Accepted saat scan dimulai, bukan menunggu worker selesai. Frontend dapat melakukan polling endpoint status atau menerima notifikasi ketika status berubah. Buat operasi finalisasi dan job scan idempoten agar retry dari klien atau sistem antrean tidak menggandakan versi mod maupun biaya pemindaian.
Publikasi, observabilitas, dan pengujian
Jangan membuat objek karantina dapat diakses publik. Setelah lulus, salin objek ke key publik yang immutable, misalnya berdasarkan ID mod, versi, dan checksum. Simpan checksum kriptografis yang dihitung server untuk integritas dan deduplikasi terkontrol. Hindari overwrite: versi mod baru harus menjadi record baru atau membutuhkan transisi eksplisit dari pemilik/moderator, bukan mengganti object key berdasarkan nama file.
Catat audit event untuk pembuatan sesi, upload selesai, penolakan validasi, hasil pemindaian, perubahan status, dan publikasi. Log terstruktur sebaiknya memuat upload_id, account_id, ukuran, checksum, kode hasil, durasi job, serta correlation ID; jangan memasukkan URL presigned atau token ke log. Pantau metrik seperti jumlah reject per alasan, kedalaman antrean, durasi scan, penggunaan kuota, dan kegagalan scanner. Lonjakan UNSAFE_ARCHIVE_PATH atau rasio kompresi mencurigakan adalah sinyal abuse yang berguna.
Uji pipeline dengan fixture aman yang mewakili serangan: entri ../, path absolut, symlink, nama dengan NUL, arsip kosong, banyak entri, header ukuran palsu, rasio kompresi tinggi, manifest invalid, dan file terlarang. Tambahkan property-based atau fuzz testing pada normalisasi path dan parser manifest. Dalam pengujian integrasi, verifikasi dua properti utama: objek gagal tidak pernah memiliki URL publik, dan retry job tidak menghasilkan lebih dari satu publikasi.
Kesalahan umum yang perlu dihindari
- Mempercayai MIME dari klien. MIME hanya petunjuk UX; validasi signature dan struktur tetap wajib.
- Mengekstrak sebelum mengecek batas. Hitung metadata lebih dahulu, lalu tetap tegakkan batas saat streaming ekstraksi.
- Menggunakan nama file sebagai key storage. Gunakan ID yang dibuat server untuk mencegah collision dan overwrite.
- Menjalankan scanner secara sinkron di request API. Ini membuat endpoint rentan timeout dan mudah menjadi sasaran resource exhaustion.
- Fail-open saat antivirus atau worker gagal. Kegagalan pemeriksaan harus mempertahankan karantina, bukan menerbitkan konten.
- Menghapus karantina terlalu cepat. Pertahankan objek dan metadata secukupnya untuk investigasi, dengan lifecycle policy agar biaya tetap terkendali.
Dengan desain ini, endpoint upload tetap responsif, object storage tidak langsung menjadi CDN untuk input mentah, dan mod hanya dapat didistribusikan setelah melewati kontrol yang dapat diaudit. Batas dan allowlist perlu ditinjau dari metrik produksi, tetapi prinsipnya tetap sama: terima sesedikit mungkin, verifikasi di lingkungan terbatas, dan publikasikan hanya setelah keputusan eksplisit.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!