Have you ever seen a contact form that sends raw user input straight into a hand-written SQL query? We have. And every time a client hands us a site built with raw queries without parameter binding, our first thought is: “One single quote is all it takes to bring down the whole database.”
SQL Injection is the oldest web vulnerability, yet it remains the most insidious — one slip and your data is gone. In Laravel, the tools to prevent it are built-in. You just need to use them properly.
We at Meteora Web have seen exposed servers and stolen data because of unprotected queries. In this guide we show you exactly how to avoid it, with copy-paste-ready code.
Why SQL Injection Still Works
An SQL Injection attack occurs when user input is concatenated directly into a query. The attacker inserts an escape character (e.g., a single quote) and alters the query’s logic.
Vulnerable example:
$email = $_POST['email'];
$query = "SELECT * FROM users WHERE email = '" . $email . "'";
$result = mysqli_query($conn, $query);If the user enters admin' --, the query becomes:
SELECT * FROM users WHERE email = 'admin' --'And the attacker logs in as admin without knowing the password.
Laravel eliminates this problem if you use its stack correctly. But you can still write dangerous code if you drop into raw SQL without parameters.
Three Levels of Protection in Laravel
1. Query Builder: The Safest and Fastest
Laravel’s Query Builder uses prepared statements under the hood. Values are sent separately from the query structure, making it impossible to alter the logic.
$users = DB::table('users')
->where('email', $request->input('email'))
->first();No matter what the user types, binding renders it harmless. This works for where, insert, update, delete.
Watch out for whereRaw and orderByRaw: If you must use raw queries, always pass parameters via associative array.
// SAFE
$users = DB::table('users')
->whereRaw('email = ?', [$request->email])
->get();
// EVEN SAFER with named parameter
$users = DB::table('users')
->whereRaw('email = :email', ['email' => $request->email])
->get();2. Eloquent ORM: Native Protection with Mass Assignable Attributes
Eloquent extends security through $fillable and $guarded, but queries are always prepared.
$user = User::where('email', $request->email)->first();Pitfalls to avoid: Don’t use firstOrFail or find with unsanitized input if you rely on implicit casting. Laravel still protects you, but get used to passing scalar variables.
For whereIn or whereBetween, use arrays:
$users = User::whereIn('id', $request->input('ids', []))->get();
// If ids is not an array, Laravel throws an exception or uses empty.3. Raw SQL with DB::statement and DB::select: Binding Saves the Day
Sometimes you need pure SQL for performance (e.g., complex reporting). Always use DB::statement or DB::select with parameters.
DB::statement('UPDATE users SET points = points + ? WHERE id = ?', [$points, $userId]);Never write:
DB::statement("UPDATE users SET points = points + {$points} WHERE id = {$userId}");
// INJECTION! If $userId is "1; DROP TABLE users--" you lose everything.Real Cases We’ve Seen
One client had a custom report built with raw queries concatenating JSON input. We rewrote everything with binding in minutes. Result: faster queries (binding allows MySQL to cache execution plans) and zero vulnerabilities.
Another case: an e-commerce site with dynamic filters. They used whereRaw without binding to sort by price. One character could expose the entire orders table. We fixed it with orderByRaw('price ?', [$direction]).
We at Meteora Web have also recovered a backup after an SQL Injection attack on a third-party website. The database was exposed with all client data. Lesson: security is not an afterthought — it’s a development baseline.
Common Mistakes Even in Laravel
- Using
DB::rawwithout binding —DB::rawis for raw SQL fragments. If you concatenate variables, you lose protection. - Ignoring validation — Even with binding, malformed input can cause errors or logic bypasses. Always validate type, format, and length.
- Not protecting stored procedures — If you call MySQL stored procedures, use prepared statements there too.
- Forgetting dynamic
orderBy—orderBy('column', $direction)is safe only if$directionis controlled (e.g., 'asc' or 'desc'). Never accept arbitrary input for the column name.
Verify That Your Queries Are Safe
You can enable Laravel’s query log to see prepared statements in action. Turn it on during development:
DB::enableQueryLog();
$users = User::where('email', $request->email)->get();
dd(DB::getQueryLog());
// You'll see 'bindings' => [':email' => '...'] and query with ? placeholdersAlso, use tools like Laravel Security Checker or RIPS for automated scans. We recommend periodic penetration testing.
In Summary — What to Do Right Now
- Audit all raw queries in your code: search for
DB::raw,whereRaw,selectRaw,orderByRaw. Convert to binding immediately. - Always use
DB::statementwith associative arrays for complex raw queries. - Enable robust validation on input fields using Laravel’s
validateorFormRequest. - Check query logs in development to verify binding.
- Run a security test with tools like
sqlmapin staging to simulate attacks.
SQL Injection isn’t a problem if you code with security in mind from the first character. Laravel gives you all the tools — use them. If you have an old PHP project to audit, we’re here to help.
Sponsored Protocol