Dari semua ancaman web, SQL injection paling terkenal karena dampaknya paling langsung: satu input yang salah penanganannya bisa membocorkan seluruh isi database, bahkan menghapusnya. Kerentanan ini lahir dari satu kebiasaan buruk — merakit string SQL dengan cara menempelkan input pengguna langsung ke dalamnya. Laravel melindungi kita dari kebiasaan itu secara bawaan: Query Builder dan Eloquent selalu mengirim nilai lewat parameter binding. Tetapi pintu darurat bernama DB::select dengan string mentah tetap terbuka, dan di sanalah kecelakaan biasa terjadi. Chapter ini mendemonstrasikan serangan sungguhan pada query rentan, lalu membuktikan bahwa input yang sama menjadi tidak berbahaya begitu binding dipakai.
Anatomi Serangan: Tanda Kutip yang Kabur #
Bayangkan fitur pencarian postingan yang merakit query dengan concatenation. Pengguna mengetik kata kunci, dan kode menempelkannya ke dalam string SQL:
use Illuminate\Support\Facades\DB;
$cari = "x' OR '1'='1";
$sqlRentan = "SELECT * FROM posts WHERE judul = '".$cari."'";
$hasil = DB::select($sqlRentan);
echo 'query asli : '.$sqlRentan."\n";
echo 'jumlah hasil : '.count($hasil)."\n";Output yang Dihasilkan:
query asli : SELECT * FROM posts WHERE judul = 'x' OR '1'='1'
jumlah hasil : 3Perhatikan string SQL akhirnya. Tanda kutip tunggal dari input menutup string 'x' lebih awal, lalu OR '1'='1' menempel sebagai kondisi yang selalu benar — query tidak lagi mencari judul, melainkan meloloskan semua baris. Pada tabel berisi tiga postingan, hasilnya:
judul bocor : Belajar Laravel, Trik Eloquent, Routing DasarVarian klasik ' OR 1=1 -- bekerja serupa: bagian -- mengomentari sisa query asli sehingga tanda kutip penutup tidak pernah dibaca. Pengujian pada query rentan yang sama menghasilkan 3 baris — seluruh tabel terbaca. Dengan kreativitas tambahan, penyerang bisa menyusun UNION SELECT untuk membaca tabel lain, termasuk tabel users berisi hash password.
Pertahanan: Parameter Binding #
Parameter binding memindahkan nilai keluar dari string SQL. Tanda tanya menjadi tempat kosong, dan nilai dikirim terpisah ke driver database — database kemudian memperlakukannya murni sebagai data, bukan perintah:
$hasil = DB::select('SELECT * FROM posts WHERE judul = ?', [$cari]);
echo 'jumlah hasil : '.count($hasil)."\n";Output yang Dihasilkan:
jumlah hasil : 0Input penyerang yang sama kini diperlakukan literal sebagai string "x' OR '1'='1" yang tidak cocok dengan judul mana pun. Kutipan, OR, dan -- kehilangan kekuatan mereka karena tidak pernah menyentuh struktur query. Inilah prinsip yang harus dipegang: input pengguna tidak pernah boleh diinterpolasi ke string SQL — selalu lewat binding.
Eloquent dan Query Builder Selalu Binding #
Pada Query Builder dan Eloquent, pembatasan ini tidak bergantung pada kedisiplinan kita. Method where() menghasilkan SQL dengan placeholder ? dan menampung nilai di daftar binding secara otomatis:
use App\Models\Post;
$q = Post::where('judul', $cari);
echo 'toSql() : '.$q->toSql()."\n";
echo 'getBindings() : '.json_encode($q->getBindings())."\n";
echo 'jumlah hasil : '.$q->count()."\n";Output yang Dihasilkan:
toSql() : select * from "posts" where "judul" = ?
getBindings() : ["x' OR '1'='1'"]
jumlah hasil : 0Method toSql() menampilkan SQL final dengan ? di posisi nilai, dan getBindings() menunjukkan payload penyerang duduk damai di daftar parameter. Seluruh keluarga where*(), insert(), update(), dan delete() di Eloquent mengikuti pola yang sama.
Pintu Darurat dan Aturannya #
Tiga escape hatch tetap tersedia, dan masing-masing punya aturan main. whereRaw() misalnya menerima string SQL mentah — dua cara memakainya menghasilkan dua dunia:
// RENTAN: nilai ditempel ke dalam string
Post::whereRaw("judul = '".$cari."'")->count();
// AMAN: nilai dikirim lewat binding
Post::whereRaw('judul = ?', [$cari])->count();Output yang Dihasilkan:
whereRaw concat : 3 baris (bocor)
whereRaw bind : 0 baris (aman)whereRaw() sendiri bukan musuh — ia dibutuhkan untuk ekspresi yang tidak bisa diekspresikan builder, seperti whereRaw('LENGTH(judul) > ?', [100]). Yang membedakan aman dan rentan hanyalah satu hal: apakah nilai pengguna masuk lewat binding atau lewat tanda kutip. DB::raw() mengikuti aturan yang sama — jangan pernah menempelkan input pengguna ke dalamnya. Kasus pencarian LIKE adalah godaan terbesar karena tanda persen memang bagian dari pola: tetap aman selama persen menempel di binding, bukan di SQL — whereRaw('judul LIKE ?', ['%'.$cari.'%']) menghasilkan 0 baris untuk payload penyerang karena input tetap diperlakukan literal.
Satu kasus khusus adalah identifier seperti nama kolom pada ORDER BY. Identifier tidak bisa dikirim lewat binding karena database butuh nama kolom nyata, bukan string data. Input penyerang yang menyusup ke posisi ini terlihat jelas pada SQL hasil:
input kolom : judul"; DROP TABLE posts; --
toSql() : select * from "posts" order by "judul""; DROP TABLE posts; --" ascGrammar Laravel membungkus identifier dalam tanda kutip sehingga serangan ini menjadi satu identifier aneh, bukan perintah DROP TABLE. Tetapi solusi yang benar bukan mengandalkan pembungkusan, melainkan whitelist — larik kolom yang diizinkan diperiksa dengan in_array() sebelum masuk orderBy().
Kerabat Dekat: Mass Assignment #
Ada satu kerabat SQL injection yang bekerja dengan pola serupa: penyerang menyuntikkan key ekstra (bukan perintah SQL) ke input yang dipercaya aplikasi. Jika controller menyerahkan $request->all() langsung ke update(), kolom seperti is_admin bisa berubah nilainya tanpa sepengetahuan kita. Mekanisme $fillable dan pola serangannya kita bongkar tuntas di chapter Mass Assignment Protection
.
Dengan query yang selalu binding, lapisan data sudah berdiri kokoh. Ancaman berikutnya pindah ke lapisan tampilan: kode yang kita kirim ke browser pengguna — kita bongkar di chapter Cross-Site Scripting (XSS) . Referensi lengkap parameter binding tersedia di dokumentasi queries .