Kodeloka
  1. Kodeloka
  2. Kelas Keamanan Aplikasi
  3. Materi

3 · Identitas · Modul 5 dari 8

Otorisasi & Kontrol Akses

Kelemahan kontrol akses menempati peringkat satu OWASP — dan hampir selalu berupa pemeriksaan yang lupa ditulis.

Autentikasi Bukan Otorisasi

Autentikasi   siapa Anda            → login berhasil
Otorisasi     apa yang boleh Anda    → boleh membuka faktur ini?

Sebagian besar kebocoran data bukan karena penyerang menembus login, melainkan karena pengguna yang sudah sah dapat mengakses data pengguna lain hanya dengan mengganti angka pada URL.

IDOR — Celah yang Paling Sering Ditemukan

// Rentan — kepemilikan tidak pernah diperiksa
app.get("/api/faktur/:id", perluLogin, async (req, res) => {
    const faktur = await db.faktur.findById(req.params.id);
    res.json(faktur);
});
// Pengguna A membuka /api/faktur/1002 → melihat faktur pengguna B

// Benar — kepemilikan menjadi bagian kueri
app.get("/api/faktur/:id", perluLogin, async (req, res) => {
    const faktur = await db.faktur.findOne({
        id: req.params.id,
        pemilikId: req.pengguna.id       // syarat, bukan pemeriksaan terpisah
    });
    if (!faktur) return res.status(404).json({ galat: "tidak ditemukan" });
    res.json(faktur);
});
💡 Jadikan kepemilikan bagian dari kueri, bukan pemeriksaan sesudahnya

Pola "ambil dulu, periksa kemudian" mudah terlupa saat menambah endpoint baru, dan sekali terlupa celahnya langsung terbuka. Bila kepemilikan menjadi syarat pada kueri itu sendiri, data milik orang lain tidak pernah terambil sejak awal. Pengembang harus sengaja menghapus syarat itu agar celahnya muncul.

⚠️ Balas 404, bukan 403, untuk sumber daya milik orang lain

Kode 403 mengonfirmasi bahwa sumber daya itu ada — cukup untuk memetakan berapa banyak faktur di sistem dan ID mana yang aktif. Membalas 404 untuk "tidak ada" maupun "bukan milik Anda" tidak membocorkan apa pun.

Otorisasi Tidak Boleh Bergantung pada Antarmuka

// Menyembunyikan tombol BUKAN kontrol akses
{pengguna.peran === "admin" && <button>Hapus</button>}

// Endpoint tetap wajib memeriksa
app.delete("/api/pengguna/:id", perluLogin, perluPeran("admin"), ...);

Menyembunyikan menu di antarmuka hanya merapikan pengalaman pengguna. Endpoint-nya tetap dapat dipanggil langsung oleh siapa pun yang mengetahui alamatnya — dan alamat itu terlihat jelas di kode JavaScript yang dikirim ke peramban.

Penetapan Massal

// Rentan — seluruh badan permintaan masuk ke basis data
await db.pengguna.update({ id: req.pengguna.id }, req.body);
// Penyerang mengirim: { "nama": "Budi", "peran": "admin" }

// Benar — hanya field yang memang boleh diubah
const { nama, telepon } = req.body;
await db.pengguna.update({ id: req.pengguna.id }, { nama, telepon });
🚫 Field sensitif ikut terbawa tanpa terlihat di kode

Menyerahkan req.body utuh ke lapisan penyimpanan berarti setiap kolom pada tabel dapat ditulis pengguna — termasuk peran, saldo, terverifikasi, atau pemilikId. Celahnya tidak terlihat saat membaca kode karena nama field yang berbahaya tidak pernah muncul di sana. Gunakan skema validasi yang menolak field tak dikenal.

const Skema = z.object({
    nama: z.string().min(1).max(100),
    telepon: z.string().regex(/^[0-9+\-\s]+$/)
}).strict();                          // tolak field di luar daftar

const data = Skema.parse(req.body);

Menempatkan Pemeriksaan di Satu Tempat

// Tersebar — mudah terlupa pada endpoint baru
app.get("/api/laporan", perluLogin, async (req, res) => {
    if (req.pengguna.peran !== "manajer") return res.status(403).end();
});

// Terpusat — bawaan menolak
const KEBIJAKAN = {
    "laporan:baca":  ["manajer", "admin"],
    "faktur:hapus":  ["admin"],
    "faktur:baca":   ["pengguna", "manajer", "admin"]
};

function perluIzin(aksi) {
    return (req, res, next) => {
        const peranBoleh = KEBIJAKAN[aksi];
        if (!peranBoleh) return res.status(500).end();   // aksi tak terdaftar → tolak
        if (!peranBoleh.includes(req.pengguna.peran)) return res.status(404).end();
        next();
    };
}
💡 Aksi yang belum terdaftar harus ditolak, bukan diizinkan

Perhatikan baris yang menolak ketika aksi tidak ditemukan di kebijakan. Bila sebaliknya — aksi tak dikenal dianggap boleh — maka setiap salah ketik nama aksi diam-diam membuka endpoint untuk semua orang. Bawaan menolak membuat kesalahan semacam itu muncul sebagai bug yang terlihat, bukan sebagai celah yang senyap.

Keamanan Tingkat Baris di Basis Data

ALTER TABLE faktur ENABLE ROW LEVEL SECURITY;

CREATE POLICY faktur_pemilik ON faktur
    USING (pemilik_id = current_setting('app.pengguna_id')::uuid);

-- Aplikasi menetapkan konteks per koneksi
SET LOCAL app.pengguna_id = '...';

Ini lapis terakhir: sekalipun sebuah endpoint lupa memeriksa kepemilikan, basis data tetap menolak mengembalikan baris milik orang lain. Berguna khususnya pada aplikasi multipenyewa.