Sejak Chapter SQL Dasar untuk Python kita menulis query dengan tanda ?. Bab ini menjawab pertanyaan yang sengaja kita tangguhkan: mengapa nilai tidak boleh dirangkai langsung ke string SQL? Jawabannya menyangkut keamanan. String SQL yang dirakit dari input pengguna membuka celah serangan bernama SQL injection — dan celah ini bertahun-tahun masuk daftar sepuluh risiko keamanan aplikasi web teratas. Kita akan mendemonstrasikan serangannya secara nyata, lalu menutupnya dengan parameterized query.
Anatomi SQL Injection #
Bayangkan layar login sederhana. Program menerima username dan password, lalu menyusun query dengan f-string — pola yang tampak tak berdosa:
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
conn.execute("INSERT INTO pengguna (username, password) VALUES ('budi', 'rahasia123')")
conn.execute("INSERT INTO pengguna (username, password) VALUES ('siti', 'kunci456')")
conn.commit()
username = "budi"
password = "' OR '1'='1"
query_berbahaya = (
f"SELECT * FROM pengguna "
f"WHERE username = '{username}' AND password = '{password}'"
)
print("Query yang terbentuk:")
print(query_berbahaya)
print("Hasil:", conn.execute(query_berbahaya).fetchall())
conn.close()Output yang Dihasilkan:
Query yang terbentuk:
SELECT * FROM pengguna WHERE username = 'budi' AND password = '' OR '1'='1'
Hasil: [(1, 'budi', 'rahasia123'), (2, 'siti', 'kunci456')]Mari kita bedah kejadiannya:
- Baris 9-10 (
username = "budi",password = "' OR '1'='1"): penyerang mengisi username yang benar dan password berisi potongan kode SQL. Tanda kutip tunggal di awal sengaja menutup string password di query. - Baris 12-15 (
query_berbahaya = f"..."): f-string menempelkan mentah-mentah input ke SQL, menghasilkanpassword = '' OR '1'='1'— bukan lagi perbandingan password, melainkan logika baru. - Baris 18 (
conn.execute(query_berbahaya).fetchall()): klausaOR '1'='1'selalu bernilai benar, sehingga kondisiWHERElolos untuk semua baris. Query mengembalikan kedua pengguna, dan program yang hanya mengecek “ada hasil” akan mengizinkan penyerang masuk.
Input satu baris itu membobol login. Varian serangan lain bisa menghapus tabel, membocorkan seluruh isi database, atau membaca data pengguna lain — pembahasan mendalam tersedia pada Chapter SQL Injection di section keamanan.
Placeholder Tanda Tanya #
Penawarnya sederhana: kirim SQL dan nilai sebagai dua hal terpisah. Tanda ? menandai slot kosong, dan nilai datang sebagai argument kedua execute() berbentuk tuple:
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
conn.execute("INSERT INTO pengguna (username, password) VALUES ('budi', 'rahasia123')")
conn.execute("INSERT INTO pengguna (username, password) VALUES ('siti', 'kunci456')")
conn.commit()
username = "budi"
password = "' OR '1'='1"
baris = conn.execute(
"SELECT * FROM pengguna WHERE username = ? AND password = ?",
(username, password),
).fetchone()
print("Hasil login:", baris)
conn.close()Output yang Dihasilkan:
Hasil login: NoneInput penyerang yang sama kini gagal total. Mesin SQLite menerima string ' OR '1'='1 sebagai data murni untuk dibandingkan dengan kolom password — tanda kutip di dalamnya tidak pernah ditafsirkan sebagai pembatas sintaks SQL. Struktur query dan isinya tidak mungkin tercampur, sehingga injection kehilangan dayanya sepenuhnya.
executemany dan Placeholder Bernama #
Dua bentuk placeholder tersedia di sqlite3. Gaya qmark memakai ? dengan tuple; gaya named memakai :nama dengan dictionary:
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("""CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga REAL NOT NULL,
stok INTEGER NOT NULL DEFAULT 0
)""")
conn.executemany(
"INSERT INTO produk (nama, harga, stok) VALUES (?, ?, ?)",
[
("Kopi Hitam 200g", 18000, 12),
("Teh Celup Isi 25", 9500, 30),
("Gula Pasir 1kg", 15500, 25),
],
)
conn.commit()
print("Setelah executemany:",
conn.execute("SELECT COUNT(*) FROM produk").fetchone()[0], "produk")
conn.execute(
"INSERT INTO produk (nama, harga, stok) VALUES (:nama, :harga, :stok)",
{"nama": "Susu UHT 1L", "harga": 19000, "stok": 15},
)
conn.commit()
hasil = conn.execute(
"SELECT nama, harga FROM produk WHERE nama LIKE ? ORDER BY nama",
("%kopi%",),
).fetchall()
print(hasil)
conn.close()Output yang Dihasilkan:
Setelah executemany: 3 produk
[('Kopi Hitam 200g', 18000.0)]Perhatikan dua hal dari program di atas. Pertama, executemany() menjalankan satu template INSERT untuk tiga tuple — satu pernyataan, tiga baris, satu kali commit(). Kedua, placeholder juga bekerja untuk nilai LIKE: pola "%kopi%" dikirim sebagai parameter, dan pencocokannya tetap tidak peka kapital untuk huruf ASCII sehingga menemukan “Kopi Hitam 200g”.
| Gaya | Penanda | Nilai dikirim sebagai | Nyaman untuk |
|---|---|---|---|
| qmark | ? | tuple (a, b) | urutan nilai yang pendek |
| named | :nama | dictionary {"nama": a} | nilai banyak, kolom opsional, keterbacaan |
Kedua gaya sama amannya; pilihan selera. Satu jebakan kecil: gaya tidak boleh dicampur dalam satu query, dan tuple berisi satu nilai wajib ditulis (nilai,) — tanda kurung dengan koma — agar tidak keliru dianggap pengelompokan biasa. Dengan query yang aman di tangan, kita siap merakit operasi CRUD lengkap pada Chapter CRUD dengan SQLite.