Query berparameter

Mengirim nilai dari aplikasi secara aman dengan query berparameter untuk mencegah SQL injection.

Daftar isi

Topik ini membahas query berparameter, yaitu bentuk pengiriman nilai dari aplikasi yang memisahkan teks SQL dari datanya, sehingga masukan pengguna tidak pernah menjadi bagian pernyataan. Pemisahan ini menutup celah SQL injection yang mengeksploitasi perakitan teks SQL, dan seluruh contohnya dijalankan dengan psycopg yang diperkenalkan pada topik sebelumnya.

Blok penyiapan dataset#

bash
# siapkan ulang tabel dan data contoh untuk topik ini
PGOPTIONS='-c client_min_messages=warning' psql -q -U postgres -d perpustakaan <<'SQL'
DROP TABLE IF EXISTS peminjaman, buku, anggota, kategori CASCADE;
CREATE TABLE kategori (id SERIAL PRIMARY KEY, nama TEXT NOT NULL UNIQUE);
CREATE TABLE anggota (id SERIAL PRIMARY KEY, nama TEXT NOT NULL, email TEXT UNIQUE, bergabung DATE NOT NULL, aktif BOOLEAN NOT NULL DEFAULT TRUE);
CREATE TABLE buku (id SERIAL PRIMARY KEY, judul TEXT NOT NULL, penulis TEXT NOT NULL, tahun INTEGER NOT NULL, kategori_id INTEGER REFERENCES kategori(id));
CREATE TABLE peminjaman (id SERIAL PRIMARY KEY, anggota_id INTEGER NOT NULL REFERENCES anggota(id), buku_id INTEGER NOT NULL REFERENCES buku(id), tanggal_pinjam DATE NOT NULL, tanggal_kembali DATE);
INSERT INTO kategori (nama) VALUES ('Pemrograman'), ('Sains'), ('Sejarah'), ('Fiksi'), ('Statistika');
INSERT INTO anggota (nama, email, bergabung, aktif) VALUES ('Ali Rahman', '[email protected]', '2024-01-15', TRUE), ('Budi Santoso', '[email protected]', '2024-03-02', TRUE), ('Citra Kirana', '[email protected]', '2024-05-20', FALSE), ('Dewi Lestari', '[email protected]', '2024-08-11', TRUE), ('Eka Pratama', NULL, '2025-01-30', TRUE);
INSERT INTO buku (judul, penulis, tahun, kategori_id) VALUES ('Dasar Basis Data', 'Sari Wulandari', 2021, 1), ('Jaringan Komputer', 'Ahmad Fauzi', 2019, 1), ('Fisika untuk Pemula', 'Rina Marlina', 2020, 2), ('Kimia Organik', 'Rina Marlina', 2022, 2), ('Sejarah Nusantara', 'Bambang Wijaya', 2018, 3), ('Cerita Kota Lama', 'Nur Aisyah', 2023, 4), ('Statistika Terapan', 'Sari Wulandari', 2022, 5), ('Algoritma dan Struktur Data', 'Ahmad Fauzi', 2021, 1);
INSERT INTO peminjaman (anggota_id, buku_id, tanggal_pinjam, tanggal_kembali) VALUES (1, 1, '2025-03-01', '2025-03-15'), (1, 7, '2025-03-05', '2025-03-20'), (2, 3, '2025-03-10', NULL), (3, 5, '2025-02-20', '2025-03-01'), (2, 1, '2025-04-02', NULL), (4, 6, '2025-04-07', NULL), (5, 8, '2025-04-09', '2025-04-25'), (4, 2, '2025-05-01', NULL);
SQL

Jalur aman dengan placeholder#

psycopg memakai placeholder %s pada teks SQL, dan nilai-nilainya dikirim sebagai parameter terpisah pada tuple, sehingga tanda kutip pada data tidak perlu ditangani secara manual oleh penulis kode. Berkas aman.py berikut mencari anggota menurut nama yang berasal dari masukan yang berbahaya sekalipun.

python aman.py
import psycopg

conninfo = "host=localhost port=5432 dbname=perpustakaan user=postgres"
masukan = "Budi'; DROP TABLE buku; --"
with psycopg.connect(conninfo) as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT nama, email FROM anggota WHERE nama = %s", (masukan,))
        print(f"baris ditemukan: {len(cur.fetchall())}")
text Keluaran
baris ditemukan: 0
bash
# jalankan pencarian aman dengan masukan berbahaya
python3 aman.py
psql -U postgres -d perpustakaan -c "SELECT COUNT(*) AS buku_masih_ada FROM buku;"
text Keluaran
baris ditemukan: 0
 buku_masih_ada
----------------
              8
(1 row)

Masukan yang mencoba menghapus tabel tidak melakukan apa-apa, karena keseluruhan teksnya dinilai sebagai satu nilai parameter yang dibandingkan dengan kolom nama, dan penghitungan buku menegaskan tabelnya utuh. Pada perakitan teks, kutipan tunggal di tengah masukan akan menutup literal dan menyisipkan pernyataan baru, dan itulah celah yang ditutup placeholder ini.

Celah perakitan teks#

Blok berikut menggambarkan bentuk yang dilarang tanpa dijalankan, yaitu menyisipkan masukan langsung ke teks SQL melalui penggabungan, agar bentuk yang harus dicari pada tinjauan kode tetap dikenali.

python rapuh.py
# tidak-dijalankan (pola ini membentuk SQL injection yang sungguhan)
import psycopg

conninfo = "host=localhost port=5432 dbname=perpustakaan user=postgres"
masukan = "Budi'; DROP TABLE buku; --"
with psycopg.connect(conninfo) as conn:
    with conn.cursor() as cur:
        cur.execute(f"SELECT nama FROM anggota WHERE nama = '{masukan}'")

Placeholder berbeda dengan penggantian teks, karena server menerima pernyataan dan nilai sebagai dua hal terpisah, sehingga struktur pernyataannya tidak dapat diubah oleh datanya. Perbedaan ini juga membuka jalan prepared statement yang dipersiapkan sekali dan dipanggil berkali-kali dengan parameter berbeda, dan PREPARE pada dokumentasi resmi memperlihatkan bentuknya di tingkat SQL.

Daftar nilai dan executemany#

Kebutuhan yang berlapis menyusul bentuk dasarnya, yaitu membandingkan dengan sekumpulan nilai pada satu query, dan menyisipkan banyak baris secara efisien. Blok berikut memakai ANY dengan daftar parameter untuk pencarian multi-id, lalu executemany untuk penyisipan berulang.

python daftar.py
import psycopg

conninfo = "host=localhost port=5432 dbname=perpustakaan user=postgres"
ids = [1, 3, 5]
with psycopg.connect(conninfo) as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT nama FROM anggota WHERE id = ANY(%s) ORDER BY id", (ids,))
        print("anggota:", ", ".join(r[0] for r in cur.fetchall()))
        cur.executemany(
            "INSERT INTO anggota (nama, email, bergabung) VALUES (%s, %s, %s)",
            [
                ("Gilang Ramadhan", "[email protected]", "2025-07-15"),
                ("Hani Puspita", "[email protected]", "2025-07-20"),
            ],
        )
    print("penyisipan selesai")
text Keluaran
anggota: Ali Rahman, Citra Kirana, Eka Pratama
penyisipan selesai

Operator ANY menerima daftar sebagai satu parameter, pengganti yang lebih bersih daripada merakit koma-koma pada klausa IN, dan executemany mengirim seluruh penyisipan dalam satu perjalanan. Anggota baru dari contoh ini tetap hidup di basis data latihan sampai blok penyiapan topik tersendiri mengembalikan datasetnya.

Kesalahan umum#

Kesalahan pertama adalah merakit klausa IN dengan penggabungan teks untuk menghindari ANY, celah yang persis sama dengan contoh rapuhnya di atas. Kesalahan kedua adalah mengira parameter melayani nama tabel dan kolom, padahal pengenal tidak boleh berasal dari masukan, dan daftar putih pengenal yang dinyatakan kode menangani kebutuhan itu.

Kesalahan ketiga adalah lupa bahwa parameter bertipe mengikuti adaptasi driver, sehingga tanggal dikirim sebagai objek tanggal dan bukan teks yang diuraikan, dan ketidakcocokan tipe terdeteksi saat kirim. Kesalahan keempat adalah mengira ekstensiORM atau pembungkus query otomatis mengamankan semua jalur, padahal banyak pustaka menyediakan pintu teks mentah yang membuka celah yang sama.

Kebutuhan yang tersisa pada pengembangan nyata adalah pengenal dinamis, seperti nama tabel yang ditentukan konfigurasi, dan psycopg menanggungnya dengan modul sql yang menyusun objek SQL yang aman untuk pengenal beserta tempat kutipnya. Modul ini melengkapi placeholder nilai, dan pemisahan keduanya menggambarkan kaidahnya secara utuh, yaitu nilai lewat parameter dan pengenal lewat komposisi yang berkutip dengan benar. Selama kedua jalur itu dipakai, tidak ada lagi teks SQL yang dirakit dengan tangan.

Ringkasan#

  • Placeholder %s dengan tuple memisahkan teks SQL dari datanya, sehingga masukan tidak pernah mengubah struktur pernyataan.
  • Percobaan injection pada jalur berparameter hanya menghasilkan pencocokan teks, dan data utuh sebagaimana diverifikasi.
  • ANY menerima daftar sebagai satu parameter, dan executemany mengirim banyak penyisipan dalam satu perjalanan.
  • Pengenal tabel dan kolom tidak pernah berasal dari masukan, dan daftar putih kode menangani kebutuhan dinamisnya.

Referensi#