Bayangin formulir kertas di bank: kolomnya sudah dicetak ("Nama:", "Jumlah:"), dan kamu cuma boleh ngisi bagian kosongnya. Apa pun yang kamu tulis di kolom kosong tetap dianggap isian, bukan perintah baru buat petugasnya. Parameterized query (sering disebut prepared statement) bekerja persis begitu: struktur query sudah dicetak dulu, input user cuma mengisi kolom kosong, jadi gak bisa ikut jadi perintah SQL.
🧪 Tebakan cepat: query aman
SELECT * FROM users WHERE username = ?dikirim input' OR '1'='1. Query-nya bocor dan mengembalikan semua user, atau gak ketemu siapa-siapa? Tebak dulu, jawabannya ada di bagian "Parameterized query: pisah query dari data".
Di lesson sebelumnya kamu lihat gimana SQL injection bekerja. Sekarang fokusnya satu: cara menulis query yang kebal. Satu teknik mengalahkan hampir semua kasus, dan teknik itu yang kita latih.
Masalahnya: query dan data nyampur
Query yang rentan menyusun SQL dengan menyambung (concatenation) input user langsung ke dalam teks query:
// RENTAN: input disambung jadi teks query
const q = "SELECT * FROM users WHERE username = '" + input + "'";
db.exec(q);
Kalau input berisi nama biasa seperti budi, query jadi ... WHERE username = 'budi', aman. Tapi kalau input berisi ' OR '1'='1, teks query-nya berubah jadi:
SELECT * FROM users WHERE username = '' OR '1'='1'
- Tanda kutip di awal input menutup string
usernamelebih cepat. - Sisanya,
OR '1'='1', sekarang jadi bagian dari SQL, bukan data.'1'='1'selalu benar, jadi WHERE-nya cocok untuk semua baris. - Hasilnya: tabel
usersyang isinyaadmin,budi,sitikebawa semua. Padahal yang diminta cuma satu nama.
Intinya: input berubah jadi kode karena dia dicampur ke teks query. Itu akar masalahnya.
Parameterized query: pisah query dari data
Perbaikannya: tulis query dengan placeholder (tanda ? atau :nama) di tempat datanya, lalu kirim nilainya terpisah. Driver database menggabungkan keduanya, dan nilai yang kamu kirim selalu diperlakukan sebagai satu data utuh, gak pernah sebagai SQL.
// AMAN: ? adalah placeholder, input dikirim terpisah
db.exec("SELECT * FROM users WHERE username = ?", [input]);
Sekarang kirim lagi input ' OR '1'='1. Query-nya tetap SELECT * FROM users WHERE username = ?, dan driver mencari user yang username-nya persis teks ' OR '1'='1 (lengkap dengan kutip dan spasinya). Gak ada user bernama begitu, jadi hasilnya 0 baris. Itu jawaban tebakannya: query gak bocor, malah gak ketemu siapa-siapa.
Di PHP pakai PDO, placeholder-nya diberi nama dan query disiapkan dulu dengan prepare():
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute([":username" => $username]);
prepare()mengirim struktur query ke database lebih dulu, sebelum data masuk. Di titik itu database sudah tahu mana query mana data.execute([...])mengirim nilainya sebagai data murni. Mau isinya nakal seperti apa pun, dia cuma dibandingkan sebagai teks.
ORM: aman secara default
Framework ORM seperti Eloquent (Laravel) atau Prisma otomatis memakai parameter binding di balik layar. Kamu nulis query dengan method, bukan dengan teks:
// Eloquent: where() otomatis mem-bind nilainya terpisah
$user = User::where('username', $username)->first();
where('username', $username) aman dari SQL injection karena $username di-bind sebagai data, sama seperti ?. Kamu gak perlu memikirkan kutip atau escape sama sekali.
Validasi tipe sebagai lapisan tambahan
Kalau sebuah input memang harus angka, periksa dulu sebelum dipakai:
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
if ($id === false) {
http_response_code(400);
exit("ID tidak valid");
}
Validasi bukan pengganti parameterized query, tapi lapisan tambahan: input yang jelas salah bentuk ditolak lebih awal.
⚠️ Jebakan yang sering ketemu
- Ngandelin escape doang. Meng-escape kutip satu per satu gampang kelewat satu tempat, dan satu yang kelewat sudah cukup. Parameterized query gak bisa lupa.
- Blacklist karakter. Memblokir kata seperti
ORatau--selalu bisa diakali. Yang benar: pisah query dari data, bukan menebak input jahat. raw()atau interpolasi di ORM.DB::select("... role = '$role'")menyambung$rolelangsung ke teks query, jadi rentan lagi walau kamu pakai Laravel. TulisDB::select("... role = ?", [$role])dengan parameter binding.- Nama tabel atau kolom dari input. Placeholder cuma buat nilai, bukan buat nama tabel/kolom. Kalau itu harus dari input, cocokkan ke daftar yang diizinkan (whitelist).
TL;DR: SQL injection dicegah dengan memisahkan query dari data. Jangan sambung input ke teks query; pakai placeholder ? atau :nama dan kirim nilainya terpisah lewat prepare() lalu execute(). Input ' OR '1'='1 jadi dicari sebagai teks literal, gak ketemu, mengembalikan 0 baris, bukan membocorkan semua baris. ORM seperti Eloquent (where('username', $username)) aman secara default karena mem-bind nilai otomatis. Escape dan blacklist gak cukup; validasi tipe cuma lapisan tambahan.