Aplikasi modern memproses data dari banyak sumber: form pengguna, parameter URL, header HTTP, pesan antrean, dan respons API pihak ketiga. Setiap titik masuk tersebut berpotensi membawa data yang tidak terduga, baik karena kesalahan input sah maupun upaya penyalahgunaan oleh pihak yang tidak berhak. Tanpa prinsip pengkodean aman yang konsisten, satu titik lemah dapat memengaruhi seluruh sistem. Pada chapter ini kita akan membahas empat prinsip dasar yang menjadi pondasi hampir seluruh praktik keamanan aplikasi: least privilege, defense in depth, fail secure, serta jangan percaya input. Setiap prinsip dijelaskan melalui contoh kode Kotlin yang dapat dijalankan dan dianalisis baris-per-baris.

Prinsip Least Privilege #

Least privilege menyatakan bahwa setiap komponen—baik pengguna, proses, maupun token—hanya boleh memiliki hak akses sekecil yang diperlukan untuk menyelesaikan tugasnya. Bila sebuah modul hanya perlu membaca konfigurasi, jangan berikan ia hak menulis. Bila sebuah token hanya dipakai untuk satu operasi, jangan berikan ruang lingkup yang lebih luas. Prinsip ini membatasi dampak saat satu komponen berhasil ditembus: penyerang hanya memperoleh hak minimum, bukan seluruh sistem.

Dalam Kotlin, kita menerapkan prinsip ini dengan data class yang membawa ruang lingkup eksplisit. Token berikut hanya dapat melakukan aksi yang tercantum pada field ruangLingkup:

kotlin Contoh: token dengan ruang lingkup minimal
data class Token(val pengguna: String, val ruangLingkup: Set<String>) {
    fun bisa(aksi: String): Boolean = aksi in ruangLingkup
}

Dengan struktur demikian, pemeriksaan izin dilakukan sekali pada satu tempat. Penambahan aksi baru hanya mungkin lewat pembuatan token baru—tidak ada jalan pintas untuk memperluas izin secara diam-diam.

Defense in Depth #

Defense in depth berarti kita menumpuk beberapa lapisan validasi sehingga kegagalan satu lapisan tidak langsung membuka celah. Lapisan pertama memeriksa format data, lapisan kedua memeriksa otorisasi, dan lapisan ketiga mungkin memeriksa rate limit. Bila validasi format lupa menangani kasus tertentu, lapisan otorisasi tetap menahan akses yang tidak diizinkan.

Pada kode utuh di bawah ini kita menerapkan dua lapisan: validasi format username dan validasi otorisasi peran terhadap aksi yang diminta. Keduanya harus lulus sebelum permintaan diproses lebih lanjut.

kotlin
// secure_coding_principles.kt
// Demonstrates: least privilege, defense in depth, fail secure, don't trust input.

// Prinsip 1: jangan percaya input. Whitelist karakter yang diizinkan.
fun validasiUsername(input: String): Boolean {
    if (input.length !in 3..32) return false
    // Whitelist: huruf, angka, garis bawah. Semua karakter lain ditolak.
    return input.all { it.isLetterOrDigit() || it == '_' }
}

// Prinsip 2: fail secure. Default deny — bila tidak ada kecocokan, kembalikan false.
fun otorisasi(peran: String, aksi: String): Boolean {
    val matrix: Map<String, Set<String>> = mapOf(
        "admin" to setOf("baca", "tulis", "hapus"),
        "editor" to setOf("baca", "tulis"),
        "viewer" to setOf("baca")
    )
    // Cek eksplisit; null-safe operator ?: mengembalikan false bila peran tidak dikenal.
    return matrix[peran]?.contains(aksi) ?: false
}

// Prinsip 3: least privilege. Token dibatasi hanya untuk satu ruang lingkup.
data class Token(val pengguna: String, val ruangLingkup: Set<String>) {
    fun bisa(aksi: String): Boolean = aksi in ruangLingkup
}

// Prinsip 4: defense in depth. Validasi dilakukan dua lapis: format lalu kebijakan.
fun prosesPermintaan(peran: String, aksi: String, namaInput: String): String {
    // Lapis 1: validasi format input.
    if (!validasiUsername(namaInput)) return "Penolakan: format input tidak valid"
    // Lapis 2: cek otorisasi peran-aksi.
    if (!otorisasi(peran, aksi)) return "Penolakan: aksi tidak diizinkan untuk peran $peran"
    return "Diterima: $namaInput ($peran) melakukan $aksi"
}

fun main() {
    val namaCalon = listOf("budi", "budi!", "ad", "siti_april_2026")
    for (nama in namaCalon) {
        println("validasiUsername($nama) = ${validasiUsername(nama)}")
    }

    println("---")
    val kasus = listOf(
        Triple("admin", "hapus", "budi"),
        Triple("editor", "hapus", "siti"),
        Triple("viewer", "baca", "dewi"),
        Triple("hacker", "hapus", "andi"),
        Triple("admin", "baca", "budi!")
    )
    for ((peran, aksi, nama) in kasus) {
        println(prosesPermintaan(peran, aksi, nama))
    }

    println("---")
    val tokenBudi = Token("budi", setOf("baca", "tulis"))
    println("Token budi bisa('hapus') = ${tokenBudi.bisa("hapus")}")
    println("Token budi bisa('baca')  = ${tokenBudi.bisa("baca")}")
}

Output yang Dihasilkan:

text
validasiUsername(budi) = true
validasiUsername(budi!) = false
validasiUsername(ad) = false
validasiUsername(siti_april_2026) = true
---
Diterima: budi (admin) melakukan hapus
Penolakan: aksi tidak diizinkan untuk peran editor
Diterima: dewi (viewer) melakukan baca
Penolakan: aksi tidak diizinkan untuk peran hacker
Penolakan: format input tidak valid
---
Token budi bisa('hapus') = false
Token budi bisa('baca')  = true

Mari kita telaah bagian-bagian kunci dari kode di atas:

  1. Fungsi validasiUsername: lapisan pertama. Panjang string harus berada pada rentang 3 sampai 32 karakter, lalu setiap karakter harus berupa huruf, angka, atau garis bawah. Pendekatan whitelist ini menolak semua karakter yang tidak secara eksplisit diizinkan, termasuk tanda baca berbahaya seperti <, >, atau '.
  2. Fungsi otorisasi: lapisan kedua. Map<String, Set<String>> menyimpan matriks peran-aksi. Operator elvis ?: mengembalikan false bila peran tidak dikenal, sehingga default bersifat menolak, bukan menerima.
  3. Fungsi prosesPermintaan: menggabungkan kedua lapisan. Bila salah satu gagal, fungsi langsung mengembalikan pesan penolakan tanpa melanjutkan ke logika bisnis. Inilah inti defense in depth: setiap lapisan independen.
  4. data class Token: ruang lingkup disimpan sebagai Set<String> yang tidak dapat diubah (immutable). Tidak ada setter, sehingga token tidak dapat dimodifikasi setelah dibuat.
  5. Pemanggilan tokenBudi.bisa("hapus"): mengembalikan false karena "hapus" tidak ada di ruang lingkup token Budi. Token tersebut hanya boleh membaca dan menulis, sesuai prinsip least privilege.

Fail Secure #

Saat sebuah kegagalan terjadi, sistem harus memilih perilaku yang paling aman. Default yang aman adalah menolak, bukan menerima. Pertimbangkan kembali fungsi otorisasi di atas: bila peran tidak ada di map, hasilnya false. Bila kita menulis ?: true (mengembalikan true saat null), peran yang tidak dikenal justru memperoleh semua hak—sebuah kebijakan yang sangat berbahaya.

Jangan Percaya Input #

Input dari sumber eksternal tidak pernah boleh dipercaya begitu saja. Aturan ini berlaku untuk form pengguna, parameter URL, header HTTP, body JSON, bahkan data dari database lain. Validasi harus dilakukan di boundary—titik pertama data masuk ke sistem kita. Setelah lolos validasi, data dapat dianggap sebagai tipe yang aman untuk diproses.

Fungsi validasiUsername menggunakan pendekatan whitelist: hanya karakter tertentu yang diterima, sisanya ditolak. Pendekatan blacklist (mendaftar karakter terlarang) lebih lemah karena sulit menganticpasi semua karakter berbahaya. Whitelist bersifat tertutup secara default; karakter baru yang belum dipikirkan akan ditolak otomatis.

Referensi Lanjutan #

Penutup #

Empat prinsip pada chapter ini—least privilege, defense in depth, fail secure, dan jangan percaya input—merupakan pondasi yang akan kita temui berulang pada chapter-chapter berikutnya. Chapter berikutnya akan masuk ke penerapan konkret: validasi input pada boundary sistem secara mendalam, termasuk pencegahan SQL injection dan command injection.