Laravel's query builder and Eloquent ORM prevent SQL injection by default - most of the time. The vulnerabilities that actually show up in real Laravel applications live in the places where a developer reaches past that default protection, usually for a legitimate reason (a complex sort, a raw aggregate query, a legacy integration) without realizing the safety net just came off. This guide walks through each one with a real vulnerable pattern and the fix, not just a rule to memorize.

SQL Injection via Raw Query Methods

Eloquent's standard methods (where(), find(), create()) parameterize values automatically. The moment a method with Raw in its name is used with a string built from user input, that protection is gone - and these methods exist precisely because the query builder can't express everything, which is exactly why they get reached for on real, non-toy features.

// Vulnerable: user input concatenated directly into a raw fragment
$sort = $request->input('sort'); // e.g. "id"
$orders = Order::orderByRaw("$sort ASC")->get();
// Attacker sends sort=(SELECT CASE WHEN (1=1) THEN id ELSE name END)
// or worse, a UNION-based extraction via a crafted column expression

// Vulnerable: the same problem inside whereRaw
$status = $request->input('status');
Order::whereRaw("status = '$status'")->get();

// Fixed: parameter binding, even inside raw fragments
Order::whereRaw('status = ?', [$status])->get();

// Fixed: allowlist the column name entirely for anything driving structure
// (column/table names can never be parameterized, only values can)
$sort = in_array($request->input('sort'), ['id', 'created_at', 'total'])
    ? $request->input('sort')
    : 'id';
Order::orderBy($sort)->get();

The orderByRaw case matters specifically because column and table names structurally cannot be parameterized in SQL - even the fixed whereRaw pattern above doesn't help when user input is meant to select a column, not provide a value. An explicit allowlist is the only correct fix for that shape of input.

Mass Assignment: From Convenience to Privilege Escalation

Eloquent's create() and update() accept an array and assign every key present in it to a model attribute, unless the model defines $fillable (an allowlist) or $guarded (a denylist). A form that only shows a "name" and "bio" field doesn't stop an attacker from adding a role field to the raw POST body if the model doesn't restrict it.

// Model with no $fillable/$guarded - every column is assignable
class User extends Model {
    // no protection defined
}

// Vulnerable controller
public function update(Request $request) {
    auth()->user()->update($request->all());
    // attacker POSTs {"bio": "hi", "role": "admin"} - both get set
}

// Fixed: allowlist on the model
class User extends Model {
    protected $fillable = ['name', 'bio'];
}

// Fixed: allowlist at the point of use, regardless of model config
auth()->user()->update($request->only(['name', 'bio']));

Defining $fillable on the model is the stronger fix because it protects every call site, including ones added later by someone who doesn't know to add ->only() at each one individually.

Insecure Deserialization

PHP's native unserialize() can be made to instantiate arbitrary classes and invoke their __wakeup() or __destruct() methods as a side effect of decoding attacker-controlled data - a well-known PHP-wide vulnerability class, not Laravel-specific, but relevant here because Laravel apps sometimes call it directly for legacy integrations (reading a serialized payload from a third-party system, a custom cache implementation) without realizing the framework's own internals deliberately avoid it.

// Vulnerable: unserializing attacker-controlled input
$data = unserialize($request->input('payload'));

// Fixed: use JSON for anything crossing a trust boundary
$data = json_decode($request->input('payload'), true);

If you must accept serialized PHP data from a legacy system you don't control, restrict allowed classes explicitly with unserialize($data, ['allowed_classes' => false]), which decodes objects as generic __PHP_Incomplete_Class instances instead of instantiating real application classes.

Local File Inclusion via Dynamic View or Storage Paths

Passing user input into view() or a filesystem path lets an attacker read files outside the intended directory using path traversal sequences, if the input isn't constrained to a known set of values.

// Vulnerable: user selects which view to render
$page = $request->input('page');
return view("pages.$page"); // page=../../../../etc/passwd%00 (older PHP)
                             // or reads unintended internal Blade templates

// Fixed: allowlist the valid values
$allowed = ['home', 'about', 'contact'];
$page = in_array($request->input('page'), $allowed) ? $request->input('page') : 'home';
return view("pages.$page");

The same pattern applies to Storage::get()/Storage::download() calls built from a user-supplied filename - validate against a known list or a strict filename pattern, never pass the raw input straight through.

Server-Side Template Injection in Custom Blade Compilation

This one is rare because most Laravel apps never compile user-supplied strings as Blade templates - but it does happen in CMS-style features that let users customize an email template or a page layout. If user input is ever passed to Blade::render() or compiled dynamically, treat it with the same suspicion as eval(): Blade directives can execute arbitrary PHP.

// Dangerous if $userTemplate is attacker-controlled
echo Blade::render($userTemplate, $data);

// Safer: use a plain templating approach for user content
// (str_replace with a fixed set of placeholders, a restricted markdown
// renderer, or a sandboxed template engine designed for untrusted input)

Checking for These From the Outside

Most of these are code-review findings, not things visible from an external scan - but their symptoms often are: an endpoint that behaves differently with a crafted payload, a response time that spikes on a boolean-blind SQL injection probe, an error message that leaks a stack trace. Our guide to detecting SQL injection covers the external testing side of the first vulnerability class above. Shieldome's Laravel security scanner and our main Laravel security guide cover the rest of the checklist. Create a free account to run your first scan.