Satu baris kode dapat merusak seluruh model kontrol akses Anda. Masukkan request body secara langsung ke dalam pembaruan database dan Anda telah memberikan pena kepada klien untuk menulis ulang skema Anda. Itulah mass assignment. Ini bukan bug eksotis atau kasus langka. Ini adalah kegagalan desain yang muncul setiap kali sebuah API memperlakukan payload sebagai kebijakan pembaruannya sendiri.
await db.users.update(req.params.id, { ...req.body });
Terlihat bersih. Menghemat pengetikan. Namun, klien mengendalikan kunci-kuncinya. Seorang penyerang dapat menambahkan "role": "admin", "accountId": "someone_else", atau "credit": 99999" ke dalam pembaruan profil yang seharusnya biasa saja. Lapisan validasi Anda mungkin memeriksa apakah nilai-nilai tersebut adalah string atau angka dan menyatakan bahwa semuanya tampak baik-baik saja. Namun, validitas bukanlah otorisasi. Seorang pengguna mungkin secara sah memiliki catatan target tersebut. Itu tidak berarti mereka berhak untuk mengedit setiap field di dalamnya.
Seperti Apa Sebenarnya Mass Assignment Itu
Bahayanya tersembunyi dalam kenyamanan. Framework dan ORM memudahkan pemetaan kunci JSON secara langsung ke kolom database. Saat Anda melakukan ini, Anda memberi tahu database untuk mempercayai klien tentang apa yang harus diubah, bukan hanya bagaimana cara mengubahnya.
Seorang pengguna yang memperbarui profil mereka mungkin mengirimkan data yang valid untuk displayName dan bio, tetapi menyelipkan role atau balance di sampingnya. Jika controller Anda hanya meneruskan objek tersebut, database akan menulis semuanya. Validasi menangkap nilai yang salah format. Namun, validasi jarang menangkap kunci yang berbahaya. Aturan bisnis yang menyatakan "pengguna ini diizinkan untuk memperbarui profil mereka" menjadi izin menyeluruh atas setiap kolom dalam baris tersebut.
Solusinya bukanlah validasi yang lebih banyak. Melainkan arsitektur yang lebih ketat.
Tiga Gerbang Keamanan
Mutasi yang aman harus melewati tiga pemeriksaan terpisah sebelum menyentuh penyimpanan.
Field yang Diterima (Allowlist)
Mulailah dengan memutuskan kunci mana saja yang akan Anda periksa. Jika sebuah field tidak ada dalam allowlist, tolak permintaan tersebut atau hapus kuncinya. Ini membalik postur default: kolom database baru bersifat non-writable (tidak dapat ditulis) sampai pengembang secara eksplisit mengeksposnya. Skema berkembang seiring waktu. Seorang rekan tim menambahkan stripeCustomerId, departmentBudget, atau flag isVerified. Dengan allowlist, kolom-kolom baru tersebut secara otomatis terlindungi dari penulisan oleh klien. Tanpa allowlist, setiap kolom baru menjadi permukaan API yang tidak disengaja.
Nilai yang Valid
Setelah Anda mengetahui field mana yang diizinkan, periksa apakah nilai-nilainya masuk akal. Apakah string zona waktu benar-benar zona waktu yang dikenali? Apakah email diformat seperti email? Apakah angkanya dalam rentang yang wajar? Ini adalah masalah higienitas data. Ini menghentikan sampah masuk ke sistem Anda, tetapi tidak menghentikan penyalahgunaan. String "admin" yang sangat valid tetap berbahaya pada field role jika orang yang salah mengirimkannya.
Transisi yang Terotorisasi
Ini adalah gerbang yang paling sering dilewati oleh banyak tim, dan di sinilah perlindungan yang sebenarnya berada. Ajukan pertanyaan yang granular: apakah aktor spesifik ini memiliki izin untuk mengubah field spesifik pada catatan spesifik ini? Bukan "apakah pengguna adalah admin?" Bukan "apakah pengguna memiliki scope write:users?" Melainkan, "apakah pengguna ini diizinkan untuk mengubah displayName mereka sendiri, tetapi tidak pernah accountId mereka?" Otorisasi per-field mencegah izin luas seperti "Editor" atau "User" menjadi kunci utama untuk setiap properti dalam baris tersebut.
Membangun Fungsi Patch
Satukan ketiga gerbang tersebut ke dalam satu pipeline. Saat permintaan patch tiba, jalankan melalui tahapan-tahapan tersebut secara berurutan.
Pertama, filter input terhadap allowlist Anda. Jika role bukan merupakan field yang diizinkan untuk endpoint ini, hentikan di sana. Tidak ada alasan untuk memvalidasi atau mengotorisasi nilai yang seharusnya tidak pernah Anda terima.
Kedua, validasi nilai yang diizinkan. Periksa tipe, format, dan aturan bisnis. Field lokasi harus berupa string yang merujuk pada zona waktu yang nyata. URL avatar harus berupa URI yang valid dengan panjang tertentu.
Ketiga, otorisasi tindakan tersebut. Verifikasi bahwa aktor memiliki catatan target tersebut, atau memegang izin tepat yang diperlukan untuk field ini. Kepemilikan adalah default yang baik untuk data pribadi, tetapi beberapa field tetap membutuhkan gerbang tambahan. Seorang pengguna mungkin memiliki profil mereka, namun hanya admin penagihan yang boleh menyentuh taxRegion.
Keempat, normalisasi data. Hapus spasi kosong (trim whitespace), gabungkan spasi yang berulang, ubah email menjadi huruf kecil, atau hapus karakter kontrol. Lakukan ini setelah validasi tetapi sebelum penyimpanan agar Anda tidak membandingkan string yang kotor selama pemeriksaan otorisasi.
Jika input gagal melewati gerbang apa pun, tolak seluruh mutasi. Jangan terapkan field yang aman secara parsial dan abaikan yang buruk secara diam-diam. Respons campuran melatih klien untuk mencoba setiap kunci yang mereka pikirkan dan melihat mana yang berhasil. Gagal secara eksplisit.
Kasus-Kasus Ekstrem (Edge Cases) yang Benar-benar Penting
Pertahanan mass assignment bergantung pada detail yang sering kali terlewatkan oleh unit test.
Kunci JSON duplikat. Penyerang dapat mengirimkan payload seperti {"role": "user", "role": "admin"}. Tergantung pada parser HTTP dan framework Anda, nilai kedua mungkin menimpa nilai pertama sebelum kode aplikasi Anda melihat objek tersebut. Uji perilaku ini pada level parser. Jika framework Anda menerima kunci terakhir secara diam-diam, allowlist Anda mungkin melihat "user" sementara database menerima "admin".
Objek bersarang (nested objects), null, dan array. Jangan berasumsi bahwa payload bersifat datar (flat). Klien mungkin membungkus field yang dibatasi di dalam objek bersarang seperti { "profile": { "role": "admin" } }. Allowlist Anda harus melakukan rekursi jika skema Anda melakukannya. Demikian pula, putuskan bagaimana Anda menangani null. Apakah itu berarti "abaikan field ini" atau "hapus field ini"? Dan jika array diharapkan, apakah validator Anda menolak struktur yang tidak terduga, atau apakah ia mengubah objek tunggal menjadi array dan membiarkannya lewat?
Normalisasi Unicode. Dua string dapat terlihat identik bagi manusia meskipun merupakan urutan byte yang berbeda. Seorang pengguna mungkin mengirimkan é yang sudah tergabung (precomposed) atau e yang terurai (decomposed) ditambah aksen penggabung. Jika pemeriksaan otorisasi Anda melakukan normalisasi sekali tetapi lapisan penyimpanan Anda melakukan normalisasi secara berbeda, Anda dapat berakhir dengan data yang tidak konsisten atau, lebih buruk lagi, bypass di mana tabrakan nama pengguna lolos dari logika Anda. Lakukan normalisasi lebih awal dan lakukan secara konsisten.
Race conditions. Keputusan otorisasi bukanlah bingkai foto yang membeku (freeze frames). Keputusan tersebut terjadi pada satu titik waktu. Dua permintaan dapat membaca catatan yang sama, keduanya melihat bahwa aktor diizinkan untuk menulis, dan keduanya mengeluarkan pembaruan. Di antaranya, status atau izin aktor mungkin telah berubah. Selalu terapkan pembaruan database dengan kondisi pada nomor versi atau nilai state machine. Gunakan sesuatu seperti UPDATE users SET ... WHERE id = ? AND version = 5. Jika baris tersebut telah berubah sejak Anda membacanya, penulisan akan gagal. Tangani kegagalan dengan mencoba lagi atau menolaknya. Hal ini mencegah pemeriksaan otorisasi yang usang merusak data Anda.
Pantau Apa yang Penting
Anda tidak dapat mengamankan apa yang tidak dapat Anda lihat. Bangun logging audit Anda di sekitar keputusan, bukan hanya tindakan.
Catat ID aktor dan ID target. Catat nama field yang tepat yang diterima dan yang ditolak. Catat versi kebijakan yang membuat keputusan dan hasil akhirnya. Jika seorang pengguna tiba-tiba mulai mengalami penolakan role dalam pembaruan profil mereka, Anda ingin mengetahuinya segera.
Jangan pernah mencatat bearer token. Jangan pernah menuangkan seluruh isi permintaan (request body) ke dalam log Anda. Jejak audit harus membantu Anda menyelidiki penyalahgunaan, bukan menjadi repositori kredensial dan data pribadi.
Satu-satunya Aturan yang Anda Butuhkan
Isi permintaan (request body) mengusulkan data. Ia tidak pernah menentukan otoritasnya sendiri. Klien dapat meminta apa saja. Server Anda memutuskan, field demi field dan baris demi baris, apa yang diizinkan untuk masuk ke penyimpanan permanen. Bangun patch Anda dengan pemisahan tersebut dalam pikiran, dan mass assignment menjadi masalah yang sudah Anda hentikan jauh sebelum mencapai lapisan otorisasi Anda.
