Laravel does a lot for you — Eloquent parameterizes queries, Blade escapes output, CSRF tokens are wired into every form helper. Yet Laravel apps still get breached, and the causes are remarkably consistent: a model with $guarded = [] that turns every column into an attacker-writable field, a {!! !!} in a Blade template rendering user input, and the perennial classic — APP_DEBUG=true left on in production, which historically turned Laravel's Ignition error page into a remote-code-execution vector (CVE-2021-3129). None of these are framework bugs. They are the seams where a developer's convenience overrides a secure default. This guide walks each one and the idiomatic way to close it.
How does mass assignment bite Laravel apps?
Through Eloquent's $fillable / $guarded configuration. When you call Model::create($request->all()), Eloquent assigns every field in the request — unless the model restricts which attributes are mass-assignable. The dangerous shortcut is protected $guarded = [];, which disables the guard entirely and lets a request set any column, including is_admin or role:
// DANGEROUS: every attribute is mass-assignable
class User extends Model {
protected $guarded = [];
}
// SAFE: explicit allowlist of assignable fields
class User extends Model {
protected $fillable = ['name', 'email', 'bio'];
}
Prefer $fillable as an explicit allowlist, and never include privilege columns in it. Better still, validate and pass only the fields you want rather than $request->all():
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
User::create($data); // only validated fields reach the model
What is the difference between {{ }} and {!! !!} in Blade?
{{ $value }} runs the value through htmlspecialchars — it is escaped and safe against XSS. {!! $value !!} renders the string as raw, unescaped HTML. That double-bang is Laravel's dangerouslySetInnerHTML: any user-controlled data passed through it is a direct XSS injection point.
{{-- Safe: escaped --}}
<p>{{ $comment->body }}</p>
{{-- Dangerous if $comment->body is user-controlled --}}
<div>{!! $comment->body !!}</div>
If you must render user-authored HTML (markdown, rich text), sanitize it with a library like HTMLPurifier before it reaches {!! !!}. Grep your Blade templates for {!! — each one is an XSS candidate until proven to render only trusted content.
Why is APP_DEBUG=true in production so dangerous?
Because debug mode exposes internals attackers weaponize. With APP_DEBUG=true, Laravel's error pages reveal stack traces, environment variables, and configuration — and historically the Ignition debug-page library shipped an endpoint that, combined with debug mode, allowed remote code execution (CVE-2021-3129, patched in Ignition 2.5.2). The defense is unambiguous: set APP_DEBUG=false and a proper APP_ENV=production in every non-development environment, keep dependencies patched, and never expose the .env file or debug tooling publicly. Confirm it from the outside — a dynamic scan of the deployed app will catch a verbose error page that a config review might miss.
Is Eloquent enough to prevent SQL injection?
For its query-builder and ORM methods, yes — they parameterize bindings. The risk is the raw escape hatches: DB::raw, whereRaw, orderByRaw, and DB::select with interpolated strings. Use bindings, never string concatenation:
// VULNERABLE: user input concatenated into SQL
User::whereRaw("email = '" . $request->email . "'")->get();
// SAFE: parameter bindings
User::whereRaw("email = ?", [$request->email])->get();
// SAFE: query builder
User::where('email', $request->email)->get();
Watch orderByRaw($request->sort) in particular — column/direction from user input is a common injection point that bindings don't fully protect; validate against an allowlist instead.
What about CSRF and authentication defaults?
Laravel's VerifyCsrfToken middleware protects web routes automatically, and the @csrf Blade directive injects the token into forms — leave both in place. When you exclude routes via the $except array, make sure you are not exempting state-changing endpoints. For authentication, lean on the maintained first-party packages (Breeze, Fortify, Sanctum) rather than hand-rolling password hashing or session handling; they use Laravel's bcrypt/Argon2 hashing and battle-tested flows.
How do file uploads and authorization go wrong?
Two Laravel-specific seams deserve attention. File uploads: validate the MIME type and extension, generate your own storage filename rather than trusting the client's, and store uploads outside the public web root (or on a dedicated disk) so an attacker can't upload a .php file and then request it to execute. Authorization: an endpoint like Invoice::find($id) that returns a record without checking ownership is broken object-level authorization — the most common API flaw. Use Laravel's Policies and Gates to enforce that the authenticated user actually owns the resource, and call $this->authorize() (or a policy) in every controller action that touches a user-scoped record, rather than assuming a route guard is enough.
Laravel security checklist
- Use
$fillableallowlists; never$guarded = []. - Validate requests and pass only validated data to models.
- Escape with
{{ }}; treat every{!! !!}as an XSS candidate. APP_DEBUG=falseandAPP_ENV=productioneverywhere but local.- Keep secrets in
.env, never committed, never web-exposed. - Use bindings for any raw SQL; allowlist
orderBycolumns. - Keep
@csrfandVerifyCsrfTokenon; audit the$exceptlist. - Run continuous SCA on the Composer dependency tree.
The Composer supply chain
Laravel apps pull in Composer packages and their transitive dependencies, and Packagist is a supply-chain target like any registry. Pin versions in composer.lock, install with composer install (not update) in CI so builds are reproducible, and layer software composition analysis across the full dependency graph so you patch reachable risk instead of drowning in advisories.
How Safeguard Helps
Safeguard resolves your complete Composer dependency tree and uses reachability analysis to surface the vulnerabilities your Laravel code actually exercises, cutting the noise from version-only scanners. Griffin, our AI analysis engine, reviews new package versions for behavioral anomalies before they reach a build, and auto-fix opens tested pull requests with the minimal safe upgrade. Mass-assignment discipline, Blade escaping, and disabling debug mode stay your responsibility — Safeguard secures the packages underneath.
Take control of your Laravel supply chain — create a free account, read the documentation, or compare Safeguard to Veracode.