Dasar Pencegahan SQL Injection di Era ORM
Salah satu asumsi yang paling sering saya temui di tim development, termasuk dari developer yang sudah cukup berpengalaman, adalah "kita pakai Eloquent, jadi sudah aman dari SQL injection". Asumsi ini setengah benar, dan bagian yang salah itulah yang justru berbahaya. ORM memang menutup celah paling umum secara default, tapi ia tidak menghapus konsep SQL injection dari daftar hal yang perlu dipikirkan seorang developer. Begitu ada kebutuhan query yang sedikit di luar pola CRUD biasa, misalnya sorting dinamis, laporan custom, atau filter kompleks, celah lama itu bisa muncul lagi meski kita tetap berada di dalam ORM.
Kenapa ORM Tidak Otomatis Aman
Secara default, method-method query builder seperti where(), find(), atau create() memang mengirim nilai sebagai parameter terikat (bound parameter) ke database, bukan menyisipkannya langsung ke string SQL. Ini yang membuat pola dasar seperti User::where('email', $email)->first() aman meski $email berasal dari input pengguna. Tapi keamanan ini adalah properti dari cara method itu dipanggil, bukan properti dari ORM itu sendiri. Begitu kita mulai menulis fragmen SQL sebagai string, sebagian atau seluruhnya, jaminan itu hilang, walaupun kita menulisnya lewat method yang disediakan ORM. Ini yang sering luput karena secara visual kodenya tetap terlihat "Laravel banget", padahal secara mekanisme sudah kembali ke raw SQL.
Titik-Titik yang Masih Rawan
Beberapa pola yang saya anggap perlu selalu dicurigai saat code review, apapun frameworknya:
- Raw query dengan concatenation.
DB::raw(),whereRaw(), atauselectRaw()yang menyisipkan variabel langsung ke string, bukan lewat binding, tetap rawan seperti query SQL manual biasa. - Nama kolom atau tabel dari input pengguna. Binding parameter hanya bekerja untuk nilai, bukan untuk identifier seperti nama kolom di
orderBy()dinamis atau nama tabel di multi-tenant sederhana. Kalau nilai ini datang dari input tanpa whitelist, itu celah injection. - Pencarian dengan wildcard LIKE. Karakter
%dan_pada queryLIKEperlu di-escape secara eksplisit kalau nilainya berasal dari input pengguna, sesuatu yang kadang terlewat meski query-nya tetap pakai binding. - Second-order injection. Data yang disimpan dengan aman lewat ORM di satu tempat, lalu diambil dan dipakai untuk menyusun raw query di tempat lain tanpa binding ulang. Ini sering luput karena titik masuk dan titik eksekusi query terpisah jauh dalam kode.
- Stored procedure atau query lintas service. Kalau aplikasi memanggil stored procedure atau meneruskan filter ke service lain yang menyusun query sendiri, jaminan ORM di sisi aplikasi tidak otomatis menular ke sana.
Kebiasaan Praktis yang Saya Pegang
Beberapa hal ini yang saya coba jadikan kebiasaan tim, bukan sekadar aturan yang ditempel di dokumen:
- Kalau terpaksa pakai raw query, tetap gunakan binding lewat parameter placeholder, jangan concatenation string, misalnya
DB::select('select * from orders where status = ?', [$status]). - Untuk nama kolom dinamis seperti sorting, pakai whitelist eksplisit, bukan lolos-in apa adanya dari query string. Sebagai ilustrasi, cek dulu apakah kolom yang diminta ada di daftar kolom yang diizinkan sebelum dipakai di
orderBy(). - Jadikan
grep -r "DB::raw\|whereRaw\|selectRaw"bagian rutin code review, bukan sekali saat audit besar. Setiap hasil yang muncul layak dicek satu per satu. - Terapkan prinsip least privilege di level user database itu sendiri, supaya kalaupun ada celah yang lolos, dampaknya terbatas.
- Jangan lupa lapisan validasi input tetap perlu berjalan meski query sudah aman secara mekanisme, karena keduanya menyelesaikan masalah yang berbeda.
Intinya
ORM menyelesaikan sebagian besar kasus SQL injection secara default, tapi bukan seluruhnya. Titik rawan biasanya muncul justru di kode yang "sedikit lebih pintar" dari CRUD standar, tempat developer merasa sudah aman karena masih menulis kode dalam gaya ORM. Kebiasaan paling murah untuk mencegahnya adalah curiga setiap kali ada string SQL yang disusun manual, dan selalu tanya: nilai ini masuk lewat binding, atau lewat concatenation?