Security 11 min read Markdown

SQL Injection in Laravel: Where Eloquent Protects You and Where It Doesn't

Eloquent and the Query Builder bind your values through PDO prepared statements, so a plain where() is safe. The trouble starts with whereRaw, dynamic orderBy, and LIKE wildcards. Here is exactly where the gaps are.

Matt King
Matt King
June 9, 2026
Last updated: June 9, 2026
SQL Injection in Laravel: Where Eloquent Protects You and Where It Doesn't

Your Laravel app uses Eloquent everywhere. No hand-written SQL. You assume SQL injection is somebody else's problem, the kind of thing that only happens in legacy PHP from 2009. Mostly, you are right. And then someone adds a sort dropdown.

Eloquent does protect you. The catch is that the protection is narrower than most developers think, and Laravel hands you half a dozen methods that punch straight through it. Let me show you exactly where the wall ends.


Why where('email', $email) is actually safe

When you write this:

// app/Http/Controllers/UserController.php
$user = User::where('email', $request->input('email'))->first();

Laravel does not concatenate the email into a SQL string. It compiles the query into a parameterised statement and sends two things to the database separately: the SQL text with a ? placeholder, and the value as a bound parameter.

The query that reaches MySQL looks like this:

select * from `users` where `email` = ? limit 1

The value travels on a different channel. PDO uses prepared statements under the hood, so the database parses the SQL structure first, then slots the data into the placeholder. A value of ' OR 1=1 -- is just a string to compare against. It is never parsed as SQL. That is the entire reason Eloquent and the Query Builder are safe by default.

This holds for almost every standard method. where, orWhere, whereIn, whereBetween, update, insert, having, value, the lot. They all bind values. You can pass the rawest, nastiest user input you like into the value position and nothing happens. Test it yourself: enable the query log and dump the bindings.

// routes/web.php — quick proof
\DB::enableQueryLog();
User::where('name', "Robert'); DROP TABLE students;--")->get();
dd(\DB::getQueryLog());
// bindings array contains the string verbatim; the SQL has a ? placeholder

The drop never executes. The whole string sits in the bindings array as a literal. So far, so good.


The danger zone: raw methods

Laravel gives you escape hatches for when the fluent builder can't express what you need. Every one of them is a potential injection point if you build the string with user input.

Here is the full list to keep in your head:

Method Binds values? Risk with interpolation
where(), whereIn() Yes Safe
whereRaw() Only if you use ? High
selectRaw() Only if you use ? High
orderByRaw() Only if you use ? High
havingRaw() Only if you use ? High
DB::raw() No High
DB::statement() Only if you pass bindings High
DB::select() Only if you pass bindings High

The pattern that kills you is string interpolation. Look at this:

// app/Http/Controllers/ReportController.php — VULNERABLE
$age = $request->input('min_age');

$users = DB::table('users')
    ->whereRaw("age > $age")
    ->get();

That $age lands directly in the SQL string. An attacker sends min_age=0 UNION SELECT password FROM users and your "minimum age" filter is now exfiltrating credentials. whereRaw did not sanitise anything. It never claimed to.

The fix is to use the binding mechanism that whereRaw actually supports. The second argument is an array of bindings, and ? placeholders pull from it:

// app/Http/Controllers/ReportController.php — FIXED
$age = $request->input('min_age');

$users = DB::table('users')
    ->whereRaw('age > ?', [$age])
    ->get();

Better still, you usually don't need raw at all. This is the same query with zero raw SQL:

// app/Http/Controllers/ReportController.php — BEST
$users = DB::table('users')
    ->where('age', '>', $request->input('min_age'))
    ->get();

The rule for every raw method is identical. Never interpolate. Always pass values through the bindings array. selectRaw('count(*) as total, ? as label', [$label]), havingRaw('total > ?', [$min]), same shape every time. If you find a "$variable" inside any raw call, you have found a bug.

DB::select and DB::statement work the same way. The clean version:

// app/Services/Reporting.php
DB::select('select * from orders where status = ? and total > ?', [
    $status,
    $minTotal,
]);

The version that gets you a CVE-shaped incident:

// app/Services/Reporting.php — VULNERABLE, do not ship
DB::select("select * from orders where status = '$status'");

If you want a refresher on how SQL injection sits inside the wider threat model, the OWASP Top 10 for Laravel walks through where injection ranks and how it chains with other flaws.


Bindings protect values, not column names

This is the trap that catches careful developers. People learn "use bindings and you're safe," then build a sortable table and reintroduce injection without writing a single raw query.

Here is the classic:

// app/Http/Controllers/ProductController.php — VULNERABLE
$products = Product::query()
    ->orderBy($request->input('sort'), $request->input('dir'))
    ->paginate(20);

No whereRaw. No DB::raw. Looks tidy. It is still injectable, because column names and sort directions are identifiers, and identifiers are never bound. Laravel cannot use a ? placeholder for a column name — SQL doesn't allow that. So orderBy drops the value into the query structure.

An attacker passing a crafted sort value can use a subquery to leak data one boolean at a time. Blind injection through an ORDER BY clause is a well-documented technique, and it does not care that the rest of your app uses Eloquent perfectly.

The only real defence for an identifier is an allowlist. Decide which columns are sortable and refuse everything else:

// app/Http/Controllers/ProductController.php — FIXED
$sortable = ['name', 'price', 'created_at'];

$sort = $request->input('sort', 'created_at');
$sort = in_array($sort, $sortable, true) ? $sort : 'created_at';

$dir = $request->input('dir') === 'asc' ? 'asc' : 'desc';

$products = Product::query()
    ->orderBy($sort, $dir)
    ->paginate(20);

Note the direction is also locked to two literal strings, never passed through from the request. The same logic applies to any place where user input picks a column: dynamic where columns, groupBy, pluck on a chosen field, whereColumn. If a request value becomes an identifier, allowlist it. There is no binding that will save you.

This bleeds into access control too. Letting a user sort by salary might be an authorisation problem as much as an injection one, which is why authorization gates and policies belong in the same review pass as your query layer.


LIKE queries and the wildcard problem

LIKE searches have two separate issues, and people conflate them.

The first is plain SQL injection, and binding solves it completely:

// app/Http/Controllers/SearchController.php
$term = $request->input('q');

$results = User::where('name', 'like', "%{$term}%")->get();

The "%{$term}%" here is a PHP string being passed as a value, so it gets bound. No SQL injection. Good.

The second issue is subtler. The % and _ characters are LIKE wildcards. If a user includes them in their search, they change what the query matches. A search for 100% matches everything starting with 100, and a lone % matches every row. That is not classic injection, but it can leak data and hammer your database with full table scans on large tables.

Escape the wildcards before building the pattern:

// app/Http/Controllers/SearchController.php — wildcard-safe
$term = $request->input('q');
$escaped = addcslashes($term, '%_\\');

$results = User::where('name', 'like', "%{$escaped}%")->get();

On MySQL the default LIKE escape character is the backslash, which is why we escape \ as well. If you set a custom escape character, declare it with like ... escape '!' semantics accordingly. The point: bind the value to stop injection, escape the wildcards to keep the search honest.


Dynamic tables, columns, and JSON paths

The identifier rule scales to everything the user might influence. A multi-tenant report that picks a table from input:

// app/Services/TenantReport.php — VULNERABLE
$table = $request->input('dataset');
$rows = DB::table($table)->get();

Allowlist it against the tables you actually expose:

// app/Services/TenantReport.php — FIXED
$allowed = ['orders', 'invoices', 'shipments'];
$table = $request->input('dataset');

abort_unless(in_array($table, $allowed, true), 404);

$rows = DB::table($table)->get();

JSON column paths are another quiet one. Laravel supports arrow syntax like where('meta->settings->theme', $value). The value is bound, but the path (settings->theme) is part of the compiled SQL. If you let a user supply the path string, you are back in identifier-injection territory. Validate the path against a known set of keys, or restrict it to a strict pattern of word characters and arrows before it ever reaches the query.


Why composer audit won't catch this

composer audit is genuinely useful, and you should run it in CI. But understand what it does: it compares your installed packages against published security advisories. It scans your dependencies. It does not read your controllers.

A whereRaw("status = '$status'") in your own ReportController is completely invisible to composer audit. The tool has no opinion on the SQL you write. It will return a clean bill of health while your hand-rolled query bleeds.

To find injection in your own code you need three things.

  • Static analysis. Larastan (the Laravel extension for PHPStan) understands Eloquent and the Query Builder. While it won't flag every interpolated raw string out of the box, running at a high level surfaces the type-confusion and unvalidated-input patterns that usually accompany SQLi, and you can add custom rules to catch raw query usage.
  • Targeted code review. Grep your codebase for the raw methods and read every hit. grep -rn "whereRaw\|selectRaw\|orderByRaw\|havingRaw\|DB::raw\|DB::statement\|DB::select" app/ takes thirty seconds and gives you the exact list of lines to audit. Any of them with a $ inside a double-quoted string is a candidate.
  • A scanner that knows Laravel. Generic SAST tools throw noise. A Laravel-aware scanner can tell the difference between a bound whereRaw('x = ?', [$y]) and an interpolated one, which is the distinction that actually matters.

If you want a structured way to work through the whole query layer, the Laravel security audit guide lays out the order to check things in.


What to do tomorrow

The single most important action: grep for every raw query method in your codebase and read each call site. Replace any interpolated value with a ? placeholder and a binding, and replace any user-supplied column, table, direction, or JSON path with an allowlist. Those two changes close almost every Laravel SQLi gap that exists in practice.

If you want a fast inventory of where the raw queries and dynamic identifiers live before you start, run a free StackShield scan and it'll flag the patterns that dependency audits skip right over. From there it's a short checklist, and the broader Laravel security checklist for 2026 covers the rest of the surface.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.

Frequently Asked Questions

Is Eloquent safe from SQL injection?

Eloquent is safe for the values you pass to standard methods like where(), update(), and insert(), because those values become bound parameters in a PDO prepared statement. The SQL text and the data travel to the database separately, so user input can never change the query structure. The protection breaks the moment you build SQL strings yourself with whereRaw, selectRaw, DB::statement, or string interpolation. It also does not cover column names, table names, or sort directions, which are never bound. Treat Eloquent as safe for values and unsafe for identifiers.

Does whereRaw prevent SQL injection in Laravel?

whereRaw does not sanitise anything on its own. If you interpolate a variable directly into the string, like whereRaw("age > $age"), that input goes straight into the query and is fully injectable. whereRaw becomes safe only when you use placeholders and pass the values as the second argument: whereRaw('age > ?', [$age]). The placeholders are bound through PDO exactly like a normal where(). The same rule applies to selectRaw, havingRaw, and orderByRaw.

Can you get SQL injection through orderBy in Laravel?

Yes, if you pass user input as the column or direction. Parameter binding only protects values, not identifiers such as column and table names. So orderBy($request->input('sort')) lets an attacker control part of the SQL structure, and orderByRaw with interpolated input is worse. The fix is an allowlist: map the incoming value against a fixed set of permitted columns and reject anything else. Never pass a raw request value as a column name or sort direction.

How do I safely use LIKE queries with user input in Laravel?

Binding the value stops SQL injection, so where('name', 'like', $term) is not injectable. The remaining issue is that the LIKE wildcards % and _ inside the user input are still treated as wildcards, which is a logic and information-disclosure problem rather than classic injection. Escape those characters before building the pattern, using addcslashes($term, '%_\\') or Laravel's Str helper, then wrap the escaped term in your own % markers. That keeps a search for "100%" from matching everything.

Will composer audit catch SQL injection in my code?

No. composer audit checks your installed packages against known security advisories, so it only finds vulnerabilities in third-party dependencies, not in the queries you write. A whereRaw with interpolated input in your own controller is invisible to it. To catch SQLi in your code you need static analysis like Larastan or PHPStan, focused code review of every raw query method, and ideally a scanner that understands Laravel's query patterns. A free StackShield scan flags the raw query and dynamic identifier patterns that dependency tools miss.

Related Security Terms

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.