Akar Masalahnya: Data Bercampur dengan Kode
Injeksi terjadi ketika data dari pengguna disatukan ke dalam string yang kemudian ditafsirkan sebagai perintah. Basis data tidak punya cara membedakan bagian mana yang ditulis pengembang dan bagian mana yang datang dari penyerang — semuanya tiba sebagai satu teks.
// Rentan const sql = "SELECT * FROM pengguna WHERE surel = '" + surel + "'"; // Masukan penyerang: ' OR '1'='1 // Kueri yang berjalan: SELECT * FROM pengguna WHERE surel = '' OR '1'='1' // → seluruh baris terambil
Solusinya: Pisahkan Kueri dari Nilai
// PostgreSQL / node-postgres
const hasil = await db.query(
"SELECT * FROM pengguna WHERE surel = $1 AND aktif = $2",
[surel, true]
);
// Python / psycopg
cur.execute(
"SELECT * FROM pengguna WHERE surel = %s AND aktif = %s",
(surel, True)
)
// Go / database/sql
row := db.QueryRow(
"SELECT * FROM pengguna WHERE surel = $1", surel)
Kueri berparameter bukan sekadar "escaping yang lebih rapi". Struktur kueri dikirim ke basis data dan diurai lebih dulu, baru nilainya dikirim terpisah. Saat nilai tiba, rencana eksekusi sudah terbentuk — tidak ada lagi tahap penguraian yang bisa dipengaruhi. Isi ' OR '1'='1 diperlakukan sebagai teks pencarian biasa, sepanjang apa pun.
Kenapa Daftar Karakter Terlarang Selalu Bocor
// Usaha yang selalu gagal surel = surel.replace(/'/g, ""); surel = surel.replace(/--/g, ""); surel = surel.replace(/;/g, "");
Penyerang punya banyak jalan memutar: pengodean heksadesimal, komentar bertingkat, sintaks khusus per basis data, karakter Unicode yang dinormalisasi belakangan. Menghapus -- tidak menutup /**/, dan menghapus tanda kutip tidak menolong pada kolom numerik yang tidak berkutip sama sekali. Pertahanan harus benar seratus persen; penyerang cukup benar satu kali.
Nama Tabel dan Kolom Tidak Bisa Diparameterkan
// TIDAK BISA — parameter hanya untuk nilai, bukan pengenal
db.query("SELECT * FROM pengguna ORDER BY $1", [kolomUrut]);
// Benar — daftar putih yang eksplisit
const KOLOM_BOLEH = { nama: "nama", tanggal: "dibuat_pada", surel: "surel" };
const kolom = KOLOM_BOLEH[kolomUrut] ?? "dibuat_pada";
const arah = urutan === "asc" ? "ASC" : "DESC";
db.query(`SELECT * FROM pengguna ORDER BY ${kolom} ${arah}`);
Karena pengenal tidak dapat diparameterkan, satu-satunya cara aman adalah memetakan masukan pengguna ke nilai yang sudah ditentukan sebelumnya. Perhatikan bahwa yang dipakai adalah nilai dari peta, bukan masukan pengguna yang sudah "diperiksa".
ORM Tidak Otomatis Aman
// Aman — dibangun lewat query builder
await Pengguna.findMany({ where: { surel } });
// Rentan — string mentah tetap string mentah
await db.$queryRawUnsafe(`SELECT * FROM pengguna WHERE surel = '${surel}'`);
// Aman — raw dengan parameter
await db.$queryRaw`SELECT * FROM pengguna WHERE surel = ${surel}`;
Tim yang memakai ORM sering menganggap dirinya kebal injeksi. Padahal setiap ORM menyediakan jalan keluar untuk kueri kompleks, dan di situlah string mentah kembali disusun dengan penggabungan. Cari queryRawUnsafe, Sequelize.literal, atau session.execute(text(...)) saat meninjau kode.
NoSQL Juga Bisa Diinjeksi
// Rentan — objek dari JSON masuk langsung ke kueri
db.pengguna.findOne({ surel: req.body.surel, sandi: req.body.sandi });
// Penyerang mengirim:
{ "surel": "[email protected]", "sandi": { "$ne": null } }
// → cocok dengan sandi apa pun
// Perbaikan — pastikan tipenya
if (typeof req.body.sandi !== "string") throw new GalatValidasi();