Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

SQL Injection Prevention in Laravel Beyond Eloquent

By Kokil Thapa | Last reviewed: September 2026

SQL injection prevention in Laravel beyond Eloquent matters the moment you leave the ORM. Eloquent and the query builder bind parameters by default, which is why many teams assume Laravel is immune. That assumption breaks down fast when you add raw SQL, dynamic ORDER BY clauses, report queries, or legacy imports. On production apps I maintain — including client portals with complex reporting — the risky code almost always lives outside standard CRUD models. This guide covers the patterns that actually keep advanced Laravel database work safe when Eloquent is not enough.

Where does SQL injection still happen in Laravel if Eloquent is safe?

Eloquent protects you when you use it idiomatically. The danger appears at the edges. Raw queries, report builders, admin search screens, and third-party package overrides are common entry points.

Laravel 13.x runs on PHP 8.3 or higher. The database layer still sends SQL strings to MySQL 9.7, PostgreSQL 18, or MariaDB 12.3. If user input becomes part of that string unescaped, the database executes attacker-controlled syntax. Eloquent never sees the query.

A typical vulnerable pattern looks innocent in a controller:

public function search(Request $request)
{
    $term = $request->input('q');

    return DB::select("SELECT * FROM users WHERE name LIKE '%{$term}%'");
}

Input like ' OR 1=1 -- changes query logic. The fix is binding, not escaping manually. Laravel passes bound values separately from the SQL template. The driver handles typing and quoting.

SQL Injection Attack PathHTTP Request?q=maliciousControllerstring concatRaw SQLunbound inputDatabaseruns attacker SQLSafe path: placeholders + bindingsUser value never parsed as SQL syntaxfix
SQL injection prevention in Laravel beyond Eloquent starts by blocking unbound user input from reaching raw SQL strings.

High-risk Laravel APIs include DB::select(), DB::statement(), whereRaw(), selectRaw(), havingRaw(), orderByRaw(), and DB::raw() inside select lists. Each accepts a SQL fragment. Values belong in the binding array, not inside the fragment string.

Authorization gaps compound the problem. SQL injection is not the same as broken access control. A safe query that returns another user's rows is still a breach. Pair database hygiene with Laravel policies and gates on every sensitive endpoint.

How do you write safe raw SQL queries in Laravel?

The rule is simple. SQL structure is code. Data is bound. Never mix them with string interpolation or concatenation.

Use positional placeholders with DB::select

use Illuminate\Support\Facades\DB;

$users = DB::select(
    'SELECT id, email FROM users WHERE status = ? AND created_at >= ?',
    ['active', $request->date('from')]
);

Each ? maps to the next binding in order. This works for DB::insert, DB::update, and DB::delete as well. Named bindings are clearer for long queries:

DB::select(
    'SELECT * FROM orders WHERE user_id = :user_id AND total > :min_total',
    ['user_id' => $userId, 'min_total' => 1000]
);

Prefer the query builder over hand-written SQL

When logic fits the builder, use it. Methods like where(), whereIn(), and join() bind values automatically:

DB::table('orders')
    ->join('users', 'users.id', '=', 'orders.user_id')
    ->where('orders.status', $status)
    ->whereDate('orders.created_at', '>=', $from)
    ->select('orders.id', 'users.email')
    ->get();

On legal-tech portals I have shipped, document search often needs full-text or complex joins. I still start with the builder. Raw SQL is the exception after profiling proves the builder cannot express the query cleanly.

Transactions and multi-statement work

Reporting imports and batch updates should run inside transactions. Binding rules do not change inside a transaction block. See Laravel database transactions and deadlocks for isolation details. A bound query remains safe whether or not it sits in DB::transaction().

Binding vs ConcatenationUnsafe"WHERE id = {$id}"Input becomes SQLBypasses escapingAudit failsNever do thisSafe"WHERE id = ?"Bindings: [$id]Driver handles typesStatic SQL for scannersDefault pattern
Parameter binding separates SQL structure from user data — the core rule for SQL injection prevention in Laravel beyond Eloquent.

When is DB::raw() safe and when does it create injection risk?

DB::raw() marks an expression as literal SQL. It does not bind anything. Use it only for trusted, static SQL fragments — aggregate functions, database-specific syntax, or column expressions with no user input.

Safe example for a select list:

DB::table('orders')
    ->select('user_id', DB::raw('COUNT(*) as order_count'))
    ->groupBy('user_id')
    ->get();

Unsafe example that looks similar:

DB::table('users')
    ->select(DB::raw("CONCAT(first_name, ' ', '{$request->input('suffix')}') as label"))
    ->get();

The suffix value must be a bound column or a separate binding. Correct approach:

DB::table('users')
    ->selectRaw("CONCAT(first_name, ' ', ?) as label", [$request->input('suffix')])
    ->get();

selectRaw, whereRaw, and siblings accept bindings as the second argument. The first argument is the SQL template. Treat it like a prepared statement.

MethodUser data in SQL stringBindings arrayVerdict
where('col', $val)NoAutomaticSafe
whereRaw('col = ?', [$val])NoRequiredSafe
whereRaw("col = '$val'")YesNoneVulnerable
DB::raw('COUNT(*)')No static SQLN/ASafe
DB::raw("$userColumn")YesN/AVulnerable
orderBy($column)Column whitelistedAutomaticSafe if validated
orderByRaw($request->sort)YesNoneVulnerable

The Laravel database documentation states that bindings protect against SQL injection. That protection disappears the moment you embed variables inside raw strings. Static analysis tools and reviewers look for that pattern first.

For PostgreSQL-specific apps, JSON operators and window functions often push teams toward raw SQL. The binding rule does not change on PostgreSQL 18. Read PostgreSQL for Laravel developers for dialect tips, but keep placeholders universal.

How do you secure dynamic filters, search, and ORDER BY clauses?

Admin panels and API list endpoints are where dynamic SQL causes the most production incidents. Users control sort columns, direction, and filter fields. Each must be validated against a whitelist.

Whitelist sort columns and directions

public function index(Request $request)
{
    $allowedSorts = ['created_at', 'name', 'status'];
    $sort = in_array($request->input('sort'), $allowedSorts, true)
        ? $request->input('sort')
        : 'created_at';

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

    return DB::table('cases')
        ->orderBy($sort, $direction)
        ->paginate(25);
}

Never pass $request->sort directly into orderByRaw(). Attackers send values like id; DROP TABLE cases; --. Whitelisting blocks that class of input entirely.

Build filters with conditional where clauses

$query = DB::table('documents');

if ($request->filled('status')) {
    $query->where('status', $request->input('status'));
}

if ($request->filled('client_id')) {
    $query->where('client_id', (int) $request->input('client_id'));
}

if ($request->filled('q')) {
    $query->where(function ($q) use ($request) {
        $q->where('title', 'like', '%' . $request->input('q') . '%')
          ->orWhere('reference', 'like', '%' . $request->input('q') . '%');
    });
}

return $query->paginate(20);

The LIKE pattern uses binding through where(). Wildcards belong in the value side. Do not build LIKE '%{$term}%' inside whereRaw unless the term is bound.

Dynamic column names need maps, not sanitization

PHP addslashes() or regex "cleaning" does not make column names safe. Map external keys to real columns:

$columnMap = [
    'date' => 'orders.created_at',
    'amount' => 'orders.total',
    'client' => 'clients.name',
];

$externalKey = $request->input('group_by', 'date');
$column = $columnMap[$externalKey] ?? $columnMap['date'];

$query->groupBy($column);

On a legal information portal, public search rarely needs raw SQL at all. When it does, whitelists and bindings are non-negotiable. Public forms attract automated probes within hours of launch.

Dynamic SQL Decision TreeUser controls field name?YesWhitelist map onlyNoBind value with ?Static SQL fragmentNever concat inputLog and reject unknown keys
Whitelisting structural SQL parts and binding all values is the standard pattern for dynamic Laravel list endpoints.

What about stored procedures, imports, and third-party packages?

Legacy integrations often call stored procedures or run bulk imports from CSV. These paths bypass Eloquent entirely.

Stored procedure calls

DB::select('CALL sp_generate_report(?, ?, ?)', [
    $officeId,
    $startDate->format('Y-m-d'),
    $endDate->format('Y-m-d'),
]);

Procedure names should be hard-coded in application code. Do not let HTTP parameters choose procedure names. That is the same class of mistake as dynamic table names.

Import pipelines

Spreadsheet imports via Laravel Excel still end up as INSERT or UPDATE statements. Validate rows in Form Requests before persistence. Batch inserts through Eloquent or the query builder with array bindings:

DB::table('products')->insert([
    ['sku' => 'A1', 'price' => 100],
    ['sku' => 'B2', 'price' => 250],
]);

Each row value is bound. Avoid building one giant SQL string from CSV cells in a loop.

Package and scope audit

Global scopes, custom macros, and reporting packages sometimes ship raw SQL examples that copy poorly into production. Review vendor source when a package accepts sort or filter strings. Replace unsafe examples with whitelisted wrappers before deploy.

API endpoints deserve the same scrutiny. Public REST APIs built with Laravel expose list parameters to clients. Document allowed sort keys in OpenAPI. Reject unknown keys with 422 responses instead of passing them to the database layer.

How do you audit a Laravel codebase for SQL injection?

Prevention at write time beats emergency patches after a scan report. Build a repeatable audit pass into every release that touches database code.

  1. Search the codebase for whereRaw, selectRaw, havingRaw, orderByRaw, DB::raw, DB::select, and string interpolation inside SQL.
  2. Flag any { variable } or concatenation (.) inside SQL strings.
  3. Verify dynamic identifiers use whitelists, not filters or escaping.
  4. Run Laravel feature tests that send malicious query strings and assert 422 or safe empty results.
  5. Enable slow query and error logging in staging. Unexpected syntax errors often reveal injection attempts early.
  6. Schedule third-party dependency updates through Composer 2.10. Security fixes in database components still matter even when your app code is clean.

Useful grep-style patterns during review:

rg "whereRaw\(|selectRaw\(|orderByRaw\(|DB::raw\(|DB::select\(" app/
rg '"\s*\.\s*\$|\'\s*\.\s*\$|\{\$' app/ --glob '*.php'

Pair static review with the regex tester when you build search patterns for CI. False positives are acceptable in lint rules. Missed vulnerabilities are not.

The OWASP SQL injection guide remains the reference for attack classes and test payloads. Laravel-specific mitigation is binding plus whitelisting, but knowing classic payloads helps you write better tests.

Security Audit PipelineCode searchManual reviewFix bindingsFeature testsCI gate: fail on new raw concatGitLab CI or equivalent pipelineDeploy via Deployer 7PHP-FPM reload after release
Repeatable audit steps catch SQL injection in Laravel beyond Eloquent before code reaches production servers.

Penetration testing still has value for high-risk systems. Client portals with payments and document storage — like those covered in our enterprise application development work — benefit from annual third-party review. Automated grep plus tests catches most developer mistakes cheaply.

Do not confuse ORM usage with full coverage. A single reporting controller with string-built SQL can expose an entire database. Redis 8.10 caching and query result caching do not sanitize inputs either. They only store whatever the vulnerable query returned.

Key Takeaways

  • Bind every user-supplied value with ? or named placeholders; never embed input in SQL strings.
  • Use DB::raw() only for static SQL fragments; pass dynamic values through selectRaw bindings.
  • Whitelist sort columns, group fields, and direction — never trust request parameters as identifiers.
  • Prefer the query builder over hand-written SQL unless profiling proves you need raw expressions.
  • Audit whereRaw, imports, stored procedures, and third-party packages on every database-related release.
  • Combine safe SQL with authorization policies so bound queries still cannot leak cross-tenant data.

People Also Ask

Is Eloquent enough to prevent SQL injection in Laravel?

For standard CRUD, yes. Eloquent and the query builder parameterize values automatically. Risk rises when you use raw methods, dynamic identifiers, or external packages that build SQL from strings. Those paths need explicit binding and whitelisting even if the rest of the app uses Eloquent.

Can Laravel Form Request validation stop SQL injection?

Validation reduces bad input but does not replace binding. A validated string can still break SQL if concatenated into a query. Use Form Requests for shape and type rules. Use placeholders for anything that reaches the database.

Are whereIn and JSON where clauses safe with user input?

Yes, when values are passed as arrays to whereIn() or as bindings to JSON methods. Unsafe patterns appear when raw JSON paths or keys come from the request without a whitelist map. Treat JSON path segments like column names.

Does Laravel 13 change SQL injection prevention compared to Laravel 12?

The binding model is unchanged. Laravel 13.x requires PHP 8.3+. Security still depends on developer discipline outside Eloquent. Upgrade for support lifecycle — Laravel 12 is supported to February 2027 — not because the ORM suddenly handles raw SQL differently.

Ship safer database code on your next Laravel release

SQL injection prevention in Laravel beyond Eloquent is not exotic security work. It is discipline at the boundaries: bind data, whitelist structure, audit raw calls, and test list endpoints with hostile input. Eloquent covers the happy path. Your reports, imports, and admin filters need the same standard.

If you want a second pair of eyes on raw SQL, API filters, or a legacy module before upgrade, review our testing and optimization services or browse related guides like Laravel API best practices and modern Laravel architecture. For ongoing hardening after launch, support and maintenance keeps production apps patched and reviewed.

Contact us to audit database-heavy Laravel code before your next deploy. You can also read more on about me, explore the full portfolio, or check SEO setup for Laravel sites so security fixes do not break indexation.

Frequently Asked Questions

Eloquent protects idiomatic CRUD, but risk sits at the edges: raw queries via DB::select and DB::statement, report builders, admin search screens, dynamic ORDER BY clauses, legacy imports, and third-party package overrides. Laravel 13.x still sends SQL strings to MySQL 9.7, PostgreSQL 18, or MariaDB 12.3. If user input becomes part of that string unescaped, the database executes attacker-controlled syntax and Eloquent never sees the query. High-risk APIs include whereRaw, selectRaw, havingRaw, orderByRaw, and DB::raw inside select lists.

Treat SQL structure as code and data as bound values. Never mix them with string interpolation or concatenation. Use positional ? placeholders in DB::select, DB::insert, DB::update, and DB::delete, mapping each placeholder to the binding array in order. Named bindings like :user_id are clearer for long queries. Prefer the query builder where logic fits, since where, whereIn, and join bind values automatically. On production apps I maintain, raw SQL is the exception after profiling proves the builder cannot express the query cleanly. Binding rules stay the same inside DB::transaction blocks.

DB::raw marks an expression as literal SQL and binds nothing. Safe use is limited to trusted static fragments such as COUNT(*) as order_count or other aggregate functions with no user input. Risk appears when variables are embedded inside the raw string, for example CONCAT with an interpolated request value. Use selectRaw, whereRaw, and siblings with the SQL template as the first argument and bindings as the second. The Laravel database documentation confirms bindings protect against injection until you embed variables inside raw strings. Reviewers and static analysis tools flag that pattern first.

Admin panels and API list endpoints are common incident sources because users control sort columns, direction, and filters. Whitelist sort columns with in_array against a fixed list and restrict direction to asc or desc. Never pass request sort directly into orderByRaw. Build filters with conditional where clauses so LIKE wildcards sit on the value side through where, not inside whereRaw strings. Map external group_by keys to real columns with an array lookup, defaulting to a safe column. addslashes and regex cleaning do not make column names safe.

For standard CRUD, yes. Eloquent and the query builder parameterize values automatically. Risk rises with raw methods, dynamic identifiers, and external packages that build SQL from strings.

No. Validation reduces bad input but does not replace binding. A validated string still breaks SQL if concatenated into a query. Use Form Requests for shape and type rules; use placeholders for anything reaching the database.

No. The binding model is unchanged. Laravel 13.x requires PHP 8.3+. Security still depends on developer discipline outside Eloquent. Upgrade for support lifecycle, not because the ORM handles raw SQL differently.

Any API that accepts a SQL fragment where values can be concatenated into the string rather than passed to a binding array. The article flags DB::select, DB::statement, whereRaw, selectRaw, havingRaw, orderByRaw, and DB::raw inside select lists. where with a column and value is safe because binding is automatic. whereRaw with a placeholder and bindings array is safe. whereRaw with embedded variables is vulnerable. Static analysis and code review should flag curly-brace interpolation and dot concatenation inside SQL strings first.

Call procedures with bound parameters using DB::select and positional placeholders, passing office IDs, dates, and other values in the binding array rather than embedding them in the CALL string. Procedure names must be hard-coded in application code. Do not let HTTP parameters choose procedure names; that is the same class of mistake as dynamic table names. Legacy integrations that bypass Eloquent entirely need the same binding discipline as ad-hoc report queries. Pair safe calls with authorization checks so a bound query cannot return another tenant's data.

Spreadsheet imports via Laravel Excel still end up as INSERT or UPDATE statements. Validate each row in Form Requests before persistence. Use batch inserts through Eloquent or the query builder with array bindings so each cell value is bound, not concatenated into one giant SQL string built in a loop from CSV cells. Run reporting imports and batch updates inside transactions; binding rules do not change inside DB::transaction. Avoid building dynamic INSERT statements by string concatenation even when the source file looks trusted.

SQL injection occurs when attacker-controlled syntax reaches the database because user input is embedded unescaped in a SQL string. Broken access control is when a correctly formed query returns rows the caller should not see. A bound, injection-safe query that lacks policy checks on the endpoint still leaks data across users or tenants. Prevention requires both database hygiene, binding plus whitelisting for raw SQL, and Laravel policies and gates on every sensitive endpoint. Fixing injection alone does not close authorization gaps on client portals or payment systems.

Search for whereRaw, selectRaw, havingRaw, orderByRaw, DB::raw, DB::select, and string interpolation inside SQL using grep-style patterns on app/. Flag any variable concatenation inside SQL strings. Verify dynamic identifiers use whitelists, not filters or escaping. Run feature tests sending malicious query strings and assert 422 or safe empty results. Enable slow query and error logging in staging. Schedule dependency updates through Composer 2.10. Pair automated review with OWASP payload knowledge for better tests. Penetration testing adds value for high-risk client portals but grep plus tests catches most developer mistakes cheaply.

Yes, when values are passed as arrays to whereIn or as bindings to JSON query methods. The unsafe pattern is when raw JSON paths or keys come from the request without a whitelist map. Treat JSON path segments the same way you treat column names: map external keys to allowed identifiers rather than sanitizing with addslashes or regex. On PostgreSQL 18 apps using JSON operators and window functions, teams often move toward raw SQL, but the binding rule does not change by database dialect. Whitelist structure, bind values.

Global scopes, custom macros, and reporting packages sometimes ship raw SQL examples that copy poorly into production. When a package accepts sort or filter strings from HTTP parameters, review vendor source before deploy and replace unsafe examples with whitelisted wrappers. Public REST APIs built with Laravel expose list parameters to clients; document allowed sort keys in OpenAPI and reject unknown keys with 422 responses instead of passing them to the database layer. Schedule dependency updates through Composer 2.10 because security fixes in database-related components still matter even when your application code is clean.

No. Redis 8.10 caching and query result caching store whatever the underlying query returned. They do not sanitize or validate inputs. If a vulnerable raw SQL query runs against the database, caching merely persists and serves the compromised result set faster. Prevention must happen at query construction: bind values, whitelist identifiers, and audit raw calls before results ever reach the cache layer. Treat caching as a performance tool, not a security control. A single reporting controller with string-built SQL can expose an entire database regardless of what sits in front of it.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: