Formulir staf bidang lain terbuka, padahal role check sudah ada
Role check menjawab siapa boleh mengelola staf, bukan baris mana yang boleh disentuh. Guard kepemilikan bidang menutup celah itu.
Ringkasan
Staf bidang lain bisa diedit oleh QA. Role check tidak cukup untuk mengelola staf. Perbaikannya dengan menambahkan guardTargetBidang.
# Formulir staf bidang lain terbuka, padahal role check sudah ada
QA menekan tombol edit pada baris staf dari bidang lain, dan formulirnya terbuka. Bukan halaman galat, bukan redirect. Formulir lengkap dengan nama, jabatan, dan tombol simpan.
Akun QA itu milik bidang Perizinan. Baris yang ia buka milik bidang Tata Ruang. Modul manajemen staf di CMS KotaPortal sudah dipasangi pemeriksaan peran: role == admin dibolehkan mengelola staf, selain itu ditolak. Pemeriksaan itu berjalan benar. Yang tidak diperiksa adalah baris mana yang sedang disentuh.
Dugaan pertama saya: role check sudah cukup.
Tidak. Pemeriksaan peran menjawab "boleh mengelola staf", bukan "boleh mengelola staf ini". Dua pertanyaan berbeda, dan yang kedua tidak pernah ditanyakan sama sekali.
OWASP menyebut pola ini horizontal privilege escalation [1]. Di OWASP API Security Top 10 2023 ia masuk sebagai API1:2023 Broken Object Level Authorization [3], peringkat pertama dari sepuluh. Versi web-nya menempatkan Broken Access Control di A01:2021 [2]. CWE-639 [4] memberi nama resminya: otorisasi horizontal. Dua pengguna punya tingkat hak akses sama, tetapi harus dicegah mengakses sumber daya satu sama lain.
Itu deskripsi persis kejadian di atas. QA dan admin bidang lain berada di tingkat yang sama. Tidak ada eskalasi vertikal. Yang bocor adalah batas antarbidang.
Perbaikannya tiga bagian.
Pertama, guardTargetBidang. Fungsi ini tidak menerima id dari request lalu memutuskan. Ia menerima record yang sudah dimuat dari database, lalu membandingkan bidang record dengan bidang aktor. Urutannya penting: setelah load, sebelum mutasi. Dipasang di Update dan Delete. Kalau guard dipanggil sebelum load, ia tidak punya apa pun untuk dibandingkan.
Kedua, query daftar. Endpoint list staf sebelumnya mengembalikan semua baris tanpa filter bidang. Artinya meski Update sudah dijaga, halaman daftar tetap memperlihatkan baris lintas bidang, dan setiap baris itu adalah tautan ke formulir edit. List dan Count di repository sekarang menerima parameter bidang. Koleksi yang tampil sama cakupannya dengan yang boleh disentuh. Tanpa langkah ini, guard hanya menutup satu pintu dari dua.
Ketiga, galat. Guard mengembalikan ErrForbidden yang sama untuk "id tidak ada" dan "id ada tapi bukan bidang Anda". Ini keputusan sadar, bukan kelalaian. Membedakan keduanya menciptakan existence oracle 403-versus-404, dan penyerang bisa memetakan id mana yang hidup. Risikonya dinilai rendah karena id berupa deret angka berurutan sehingga enumerasi tetap murah. Trade-off diterima dan dicatat, bukan disembunyikan.
Ada satu pengecualian yang saya pertahankan sengaja. Akun warga, role=user, bukan data bidang. Mereka tidak menempel ke bidang mana pun dan tetap terlihat serta bisa diedit lintas bidang. Yang di-scope adalah data staf. Kalau guard yang sama dipasang ke akun warga, admin bidang Perizinan tidak akan bisa menolong warga yang salah daftar bidang.
Yang saya bawa keluar
Pemeriksaan peran dan kepemilikan baris adalah dua lapisan berbeda. Yang pertama menjawab "siapa Anda", yang kedua "apakah ini milik Anda". Banyak modul berhenti di lapisan pertama dan merasa selesai.
Endpoint list adalah permukaan BOLA tersendiri. Orang cenderung memasang guard di Update dan Delete lalu lupa bahwa List membocorkan hal yang sama, hanya tanpa mutasi. Filter bidang harus ada di query, bukan di komponen tampilan.
Guard di service layer, setelah load. Kalau guard ditulis di handler HTTP, setiap jalur baru seperti job terjadwal, endpoint bulk, atau CLI admin harus mengingatnya sendiri. Setelah load dan sebelum mutasi, semua jalur lewat titik yang sama.
Guard ini tidak datang sendirian. Satu berkas uji unit 157 baris ikut masuk di commit yang sama: admin satu bidang mencoba Update dan Delete akun bidang lain, superadmin dan aktor global dilewati, akun warga tak tersentuh guard. Uji repository dan handler disesuaikan di commit yang sama. Guard yang tidak diuji akan hilang di refactor berikutnya; ujian itulah yang membuatnya tinggal.
[1] OWASP Authorization Cheat Sheet
[2] OWASP Top 10 2021 A01: Broken Access Control
[3] OWASP API Security Top 10 2023, API1: BOLA
[4] CWE-639: Authorization Bypass Through User-Controlled Key