Mencegah SQL Injection
Pahami serangan injeksi SQL dan cara menutupnya dengan prepared statement.
Analogi: Perintah yang Disusupi
Bayangkan kamu menyuruh asisten: "ambilkan berkas milik [NAMA]". Kalau [NAMA] diisi "Budi", aman. Tapi kalau diisi "Budi, dan juga bakar semua berkas", asisten yang bodoh menjalankannya mentah-mentah. SQL injection persis itu: input user disambung langsung ke query SQL, dan penyerang menyisipkan perintah SQL jahat di dalam inputnya.
<?php
// Contoh 1: kode rentan vs aman (mysqli)
// RENTAN: input disambung langsung ke query
$u = $_POST["username"]; // penyerang isi: admin" OR "1"="1
$p = $_POST["password"];
$q = "SELECT * FROM users WHERE username='$u' AND password='$p'";
// Query jadi: ... WHERE username='admin" OR "1"="1' ... -> LOLOS tanpa password!
// AMAN: prepared statement, data dan perintah DIPISAH
$stmt = $db->prepare("SELECT * FROM users WHERE username=? AND password=?");
$stmt->bind_param("ss", $u, $p); // "ss" = dua-duanya string
$stmt->execute();Prepared statement mengirim struktur query dan data secara terpisah ke database. Database mengkompilasi strukturnya dulu, lalu data hanya diperlakukan sebagai nilai, tidak pernah sebagai perintah. Input " OR "1"="1 menjadi sekadar string aneh yang dicari, bukan kode yang dijalankan.
<?php
// Contoh 2: PDO (cara modern, lebih disarankan)
$pdo = new PDO("mysql:host=localhost;dbname=toko", "root", "",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// Named parameter: lebih jelas untuk banyak input
$stmt = $pdo->prepare(
"SELECT * FROM produk WHERE kategori = :kat AND harga < :maks"
);
$stmt->execute([":kat" => $kategori, ":maks" => $maksHarga]);
$hasil = $stmt->fetchAll(PDO::FETCH_ASSOC);
// INSERT aman juga wajib prepared
$stmt = $pdo->prepare("INSERT INTO users (nama, email) VALUES (:nama, :email)");
$stmt->execute([":nama" => $nama, ":email" => $email]);Aturannya absolut: setiap query yang mengandung data dari luar (form, URL, cookie, file) wajib prepared statement. Tanpa pengecualian. Query dengan nilai tetap (tanpa input luar) boleh langsung.
Jebakan yang Sering Terjadi
Escape manual sebagai pengganti. mysqli_real_escape_string() atau addslashes() terlihat cukup, tapi gagal di edge case (charset aneh, integer tanpa kutip). Prepared statement menutup celah secara struktural, bukan dengan tambal sulam. Jangan pernah mengandalkan escaping manual.
Prepared untuk nilai, tapi nama tabel/kolom disambung. Placeholder ? hanya untuk nilai, tidak bisa untuk nama tabel/kolom (ORDER BY ? tidak valid). Untuk nama kolom dinamis (misalnya sort), validasi terhadap daftar putih: in_array($kolom, ["nama", "harga"], true).
Checklist Query Aman
Tempel checklist ini di dinding sebelum menulis query apa pun:
- Ada data dari luar (form/URL/cookie)? -> wajib prepared statement.
- Nilai tetap tanpa input luar? -> query langsung boleh.
- Nama tabel/kolom dinamis? -> whitelist
in_array, bukan placeholder. - Butuh LIKE dengan input user? ->
WHERE nama LIKE ?dengan param"%$cari%"(persen di data, bukan di query!). - Error database ke user? -> jangan pernah tampilkan pesan asli; log saja.
<?php
// LIKE yang aman: wildcard di parameter, bukan di SQL
$cari = "%" . trim($_GET["q"] ?? "") . "%";
$stmt = $pdo->prepare("SELECT * FROM produk WHERE nama LIKE :cari");
$stmt->execute([":cari" => $cari]);Menaruh % di query (LIKE '%?%') tidak jalan; placeholder harus utuh. Solusinya selalu gabungkan wildcard ke nilai parameter.
Catatan teknis: Serangan SQL injection klasik:
' OR '1'='1(bypass login),'; DROP TABLE users; --(hapus tabel),UNION SELECT(curi data tabel lain). Dengan prepared statement yang konsisten, semuanya menjadi string tidak berbahaya.
Tantangan
Amankan query login
Ubah query rentan $q = "SELECT * FROM users WHERE username='$u'"; menjadi prepared statement PDO dengan named parameter :u, lalu fetch hasilnya sebagai array asosiatif.