ngodingdulu
SecurityTutorialPemula

SQL Injection: Cara Kerja dan Pencegahannya

Cukup satu tanda kutip di kolom login untuk membuka seluruh isi tabel. Lihat sendiri cara kerjanya, lalu tutup celahnya dengan satu kebiasaan sederhana.

Oleh Afa Hernandes6 menit baca

Daftar isi

SQL injection adalah salah satu celah keamanan tertua, dan sampai sekarang masih masuk daftar OWASP Top 10 sebagai bagian dari kategori Injection. Celah ini muncul setiap kali input dari pengguna ditempelkan langsung ke dalam perintah SQL. Akibatnya bisa sangat parah: data seluruh pengguna terbaca, login bisa dilewati, bahkan data bisa diubah atau dihapus.

Kabar baiknya, pencegahannya sederhana dan berlaku di hampir semua bahasa. Di tutorial ini kamu akan melihat sendiri serangan itu bekerja pada database contoh, lalu menutup celahnya. Semua contoh memakai SQLite, jadi bisa langsung dicoba tanpa memasang server database.

Contoh celahnya

Bayangkan fitur sederhana: mencari pengguna berdasarkan email. Kode yang rentan biasanya terlihat seperti ini:

sqli.py
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT, nama TEXT, peran TEXT)")
db.executemany(
"INSERT INTO pengguna (email, nama, peran) VALUES (?, ?, ?)",
[("admin@toko.id", "Admin", "admin"), ("budi@mail.id", "Budi", "pelanggan"), ("sari@mail.id", "Sari", "pelanggan")],
)
def cari_rentan(email):
# ❌ RENTAN: input pengguna ditempel langsung ke dalam teks SQL
sql = f"SELECT nama, peran FROM pengguna WHERE email = '{email}'"
return db.execute(sql).fetchall()
print(cari_rentan("budi@mail.id"))

Dengan input normal, hasilnya benar:

[('Budi', 'pelanggan')]

Sekarang coba input ini di kolom email: ' OR '1'='1

print(cari_rentan("' OR '1'='1"))
[('Admin', 'admin'), ('Budi', 'pelanggan'), ('Sari', 'pelanggan')]

Semua pengguna terbaca, termasuk admin. Hasil di atas bukan ilustrasi, tapi keluaran asli saat kode ini dijalankan dengan Python 3.13.

Kenapa ini terjadi

Lihat query yang benar-benar dikirim ke database setelah input ditempelkan:

SELECT nama, peran FROM pengguna WHERE email = '' OR '1'='1'

Tanda kutip di awal input menutup teks email lebih awal, lalu sisa input menjadi bagian dari perintah SQL. Kondisi '1'='1' selalu benar, sehingga WHERE meloloskan setiap baris.

Inilah inti SQL injection: database tidak bisa membedakan mana bagian perintah yang kamu tulis dan mana data yang diketik pengguna, karena keduanya sudah tercampur menjadi satu teks.

Di form login, trik yang sama bisa dipakai untuk masuk tanpa password. Dengan bentuk serangan lain, penyerang bisa membaca tabel lain, mengubah data, atau menghapus tabel.

Cara mencegahnya: prepared statement

Solusinya adalah jangan pernah mencampur perintah dan data. Kirim query dengan placeholder (tanda ? atau nama parameter), lalu kirim datanya terpisah. Teknik ini disebut prepared statement atau parameterized query.

Python (sqlite3)

sqli.py
def cari_aman(email):
# ✅ AMAN: SQL dan data dikirim terpisah lewat placeholder ?
return db.execute("SELECT nama, peran FROM pengguna WHERE email = ?", (email,)).fetchall()
print(cari_aman("budi@mail.id")) # [('Budi', 'pelanggan')]
print(cari_aman("' OR '1'='1")) # []

Input serangan sekarang dianggap data biasa: dicari email yang isinya persis ' OR '1'='1, dan tentu saja tidak ada. Hasilnya kosong.

Node.js (node:sqlite)

Node.js 24 punya modul SQLite bawaan. Polanya sama:

sqli.mjs
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT, nama TEXT, peran TEXT)');
const tambah = db.prepare('INSERT INTO pengguna (email, nama, peran) VALUES (?, ?, ?)');
tambah.run('admin@toko.id', 'Admin', 'admin');
tambah.run('budi@mail.id', 'Budi', 'pelanggan');
function cariRentan(email) {
// ❌ RENTAN: input pengguna ditempel langsung ke dalam teks SQL
return db.prepare(`SELECT nama, peran FROM pengguna WHERE email = '${email}'`).all();
}
function cariAman(email) {
// ✅ AMAN: SQL dan data dikirim terpisah lewat placeholder ?
return db.prepare('SELECT nama, peran FROM pengguna WHERE email = ?').all(email);
}
console.log(cariRentan("' OR '1'='1").length); // 2: semua baris bocor
console.log(cariAman("' OR '1'='1").length); // 0

Perhatikan jebakannya: versi rentan juga memakai db.prepare(). Yang membuatnya rentan adalah template string ${email} yang menempelkan input ke dalam SQL sebelum di-prepare. Memanggil fungsi bernama “prepare” tidak otomatis aman. Yang aman adalah memakai placeholder.

Library database lain di Node (misalnya untuk MySQL atau PostgreSQL) punya pola yang sama: query dengan ? atau $1, lalu nilainya di argumen terpisah.

PHP (MySQLi dan PDO)

Di PHP, gunakan prepared statement dari MySQLi atau PDO. Contoh lengkap CRUD dengan prepared statement ada di tutorial CRUD PHP MySQLi. Kalau kamu menemukan kode lama seperti ini:

// ❌ RENTAN
$hasil = mysqli_query($koneksi, "SELECT * FROM pengguna WHERE email = '$_POST[email]'");

ganti dengan:

// ✅ AMAN
$stmt = $koneksi->prepare('SELECT * FROM pengguna WHERE email = ?');
$stmt->bind_param('s', $_POST['email']);
$stmt->execute();
$hasil = $stmt->get_result();

Bagaimana dengan nama kolom dan tabel?

Placeholder hanya bisa dipakai untuk nilai, bukan untuk nama tabel, nama kolom, atau kata kunci SQL seperti ASC/DESC. Kalau fitur pengurutan memakai input pengguna (misalnya ?urut=harga), jangan tempel input itu ke SQL. Cocokkan dengan daftar yang diizinkan:

urut.py
KOLOM_BOLEH = {"harga": "harga", "nama": "nama", "terbaru": "dibuat_pada"}
def ambil_produk(urut):
kolom = KOLOM_BOLEH.get(urut, "nama") # nilai di luar daftar diganti bawaan
return db.execute(f"SELECT nama, harga FROM produk ORDER BY {kolom}").fetchall()

Yang masuk ke query hanya nilai dari daftar milikmu sendiri, tidak pernah teks dari pengguna.

Lapisan pertahanan tambahan

Prepared statement adalah pertahanan utama. Tambahkan lapisan berikut untuk membatasi kerusakan kalau ada yang terlewat:

  • Akun database dengan hak minimum. Aplikasi yang hanya membaca dan menambah data tidak perlu hak untuk menghapus tabel.
  • Validasi input sesuai bentuknya. Email harus berbentuk email dan ID harus angka. Validasi bukan pengganti prepared statement, tapi menolak banyak input aneh sejak awal.
  • Jangan tampilkan pesan error database ke pengguna. Pesan seperti syntax error near "OR" memberi petunjuk berharga bagi penyerang. Catat detailnya di log server saja.
  • ORM atau query builder (misalnya Prisma, Drizzle, SQLAlchemy, atau Eloquent) memakai parameter secara otomatis, selama kamu tidak menyusun SQL mentah dari input pengguna.

Kalau kodenya ditulis AI

AI cukup sering menghasilkan query yang menempelkan variabel langsung ke SQL, terutama untuk fitur pencarian dan filter. Saat meninjau kode buatan AI, cari pola ini:

  • f-string atau .format() di dalam teks SQL (Python),
  • template string `...${...}...` atau penggabungan + di dalam SQL (JavaScript),
  • variabel di dalam string query (PHP).

Kalau ketemu, minta AI: “Ubah semua query agar memakai prepared statement dengan placeholder. Jangan ada input pengguna yang digabung ke string SQL.” Lalu uji lagi dengan input ' OR '1'='1. Hasilnya harus kosong atau error validasi, bukan semua data.

Pertanyaan yang sering diajukan

Apakah mengganti tanda kutip (escape) sudah cukup?

Tidak disarankan. Fungsi escape mudah terlupa di satu tempat, mudah salah untuk konteks tertentu, dan tidak melindungi nilai numerik yang tidak diberi tanda kutip. Prepared statement memisahkan perintah dan data di tingkat database, jadi jauh lebih andal.

Apakah database NoSQL kebal SQL injection?

Kebal terhadap SQL injection, tapi tidak terhadap injection secara umum. MongoDB, misalnya, punya celah serupa kalau objek dari input pengguna langsung dipakai sebagai filter query. Prinsipnya sama: jangan biarkan input pengguna menentukan struktur perintah.

Bagaimana cara mengecek aplikasiku rentan atau tidak?

Mulailah dengan meninjau kode: cari semua tempat query dibuat, dan pastikan tidak ada yang menggabungkan input pengguna ke string SQL. Lalu uji form dan parameter URL milikmu sendiri dengan input seperti ' dan ' OR '1'='1. Uji hanya aplikasi milikmu sendiri: menguji sistem orang lain tanpa izin adalah tindakan ilegal.

Kesimpulan

SQL injection terjadi ketika input pengguna ditempelkan ke dalam perintah SQL, sehingga database tidak bisa membedakan perintah dan data. Satu tanda kutip sudah cukup untuk membocorkan seluruh tabel. Pencegahannya satu kebiasaan: selalu pakai placeholder, dan untuk nama kolom pakai daftar yang diizinkan.

Langkah berikutnya di jalur Security: saudara SQL injection yang terjadi di browser, XSS, dan serangan CSRF.

Bagikan:WhatsAppTelegramXFacebook
Ilustrasi password kopi-susu-123 dan salt acak masuk ke mesin hashing scrypt atau argon2id, menghasilkan hash yang disimpan di database dan tidak bisa dibalik
SecurityTutorial

Menyimpan Password dengan Benar: Hashing

Kalau database-mu bocor, password pengguna tidak boleh ikut terbaca. Begini cara menyimpannya supaya tetap aman, bahkan dalam skenario terburuk.

23 Februari 20266 mnt

Ilustrasi kotak komentar berisi kode img onerror yang ditampilkan sebagai teks dengan textContent, dan form dari situs penyerang yang gagal mengirim cookie bertanda SameSite=Lax
SecurityTutorial

XSS dan CSRF dengan Contoh Nyata

Dua serangan yang memanfaatkan browser penggunamu sendiri. Kami mencobanya di Chrome sungguhan, supaya kamu tahu persis mana yang aman dan mana yang tidak.

23 Maret 20266 mnt

Enter bukaEsc tutupHalaman pencarian