Introduction
Security vulnerabilities don't always announce themselves—they hide in everyday patterns that slip past code review. We've seen production incidents where a simple oversight in Laravel configuration, a raw SQL query, or a misused Blade directive opened the door to data theft or account takeover. This Laravel security checklist: patterns, pitfalls and fixes draws from real audits across e-commerce, SaaS, and travel platforms to show you what to watch for and how to fix it before it reaches production.
We'll walk through four critical areas: mass assignment, SQL injection, cross-site scripting (XSS), and secret management. Each one has cost us or our colleagues time—sometimes money. Let's learn from those mistakes so you don't repeat them.
Mass Assignment: Protect Against Hidden Bulk Updates
Mass assignment happens when you pass user input directly to model creation or update methods without filtering. Laravel makes this easy to do—and easy to get wrong.
Imagine a user registration endpoint that accepts an array of attributes:
// DANGEROUS: mass assignment without protection
$user = User::create($request->all());
If your User model has an is_admin column, a savvy attacker can POST:
{
"name": "Alice",
"email": "alice@example.com",
"is_admin": 1
}
And suddenly Alice is an administrator. This happened on a marketplace platform where users could add arbitrary roles to themselves.
Fix 1: Use $fillable. Explicitly whitelist which fields can be mass-assigned:
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
}
// Now this is safe—is_admin is ignored
$user = User::create($request->all());
Fix 2: Validate and assign manually. For stricter control, validate the request and assign properties individually:
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = new User();
$user->name = $validated['name'];
$user->email = $validated['email'];
$user->password = Hash::make($validated['password']);
$user->save();
Fix 3: Use $guarded as a backstop. If you prefer to deny by default, use $guarded:
protected $guarded = ['is_admin', 'created_at', 'updated_at'];
However, we recommend $fillable: it's explicit and forces you to think about each field.
SQL Injection: Trust Eloquent Bindings, Never Raw Queries
SQL injection is less common in Laravel projects because Eloquent uses parameterized queries by default. But raw SQL, string interpolation, and string concatenation still lurk in legacy code and ad-hoc reports.
We once found this in a ride-hailing platform's analytics dashboard:
// DANGEROUS: user input in raw SQL
$rides = DB::select("SELECT * FROM rides WHERE driver_id = " . $request->input('driver_id'));
An attacker could pass driver_id=1 OR 1=1 and retrieve every ride in the system. A few years ago, a similar flaw in a travel booking site leaked millions of itineraries.
Fix: Always use Eloquent or parameterized queries with bindings.
Eloquent is safe by default:
// SAFE: Eloquent handles parameterization
$rides = Ride::where('driver_id', $request->input('driver_id'))->get();
If you must write raw SQL, use bindings:
// SAFE: placeholders and bound values
$rides = DB::select(
"SELECT * FROM rides WHERE driver_id = ?",
[$request->input('driver_id')]
);
// Or use named bindings
$rides = DB::select(
"SELECT * FROM rides WHERE driver_id = :driver_id",
['driver_id' => $request->input('driver_id')]
);
Never concatenate or interpolate user input into SQL strings. Period.
XSS: Escape by Default with Blade
Laravel's Blade templating engine escapes output by default with {{ }}, converting dangerous characters to HTML entities. But developers often reach for the unescaped syntax {!! !!} to render HTML—and if that HTML comes from user input, XSS is waiting.
We found this in a CRM system's comment section:
<div>
{!! $comment->body !!}
</div>
A user posted: <script>fetch('/api/steal-data', {headers: {Authorization: document.cookie}})</script>
When a support agent viewed the comment, their session token was exfiltrated. The attacker could then impersonate them.
Fix 1: Use {{ }} for all user-generated content.
<div>
{{ $comment->body }}
</div>
```
<p>This converts <code><</code> to <code><</code>, so the script tag displays as text instead of executing.</p>
<p><strong>Fix 2: If you need rich HTML, sanitize it first.</strong> Use a library like HTMLPurifier or Laravel's built-in methods:</p>
<pre><code class="language-php">// Sanitize HTML before storing or rendering
$cleaned = trim(strip_tags($comment->body, '<b><i><em><strong><a><p></p><ul><li>'));
// Or use a proper library
$purifier = new HTMLPurifier();
$clean = $purifier->purify($comment->body);
</li></ul></a></strong></em></i></b>Then render the cleaned output:
{!! $clean !!}
Fix 3: Content Security Policy headers. Add a CSP header to your middleware to block inline scripts:
// In your middleware
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
This prevents scripts injected via XSS from running, even if they're in the DOM.
Secrets: Keep Credentials Out of Git
Every Laravel project has secrets: database passwords, API keys, encryption keys. Commit them to Git and you've leaked them forever—GitHub, GitLab, and every clone of your repo now contains them.
We've responded to incidents where developers committed .env files with AWS credentials, allowing attackers to spin up mining rigs on the victim's account. Another developer hardcoded a Stripe secret key, which was live for three days before being caught in code review.
Fix 1: .env and .gitignore. Add .env to your .gitignore:
# .gitignore
.env
.env.local
.env.*.local
Check that it's working:
git status # Should not show .env
Fix 2: Provide a .env.example template. Commit a sample file so teammates know which variables are required:
# .env.example
APP_NAME=Laravel
APP_KEY=
APP_DEBUG=false
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=myapp
DB_USERNAME=
DB_PASSWORD=
STRIPE_SECRET_KEY=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
Fix 3: Rotate secrets if they're ever exposed. Use your hosting provider's dashboard or CLI to regenerate API keys, database passwords, and encryption keys immediately.
Fix 4: Use environment-specific vaults for production. Don't rely on .env files in production. Use:
- AWS Secrets Manager or Parameter Store
- HashiCorp Vault
- Your platform's built-in secrets management (Heroku Config Vars, Railway Secrets, etc.)
These services audit access, support key rotation, and integrate with your deployment pipeline.
Common Mistakes
We see these patterns repeat across codebases:
- Forgetting $fillable on new models. You add a column, forget to add it to $fillable, and developers use $request->all() anyway.
- Raw queries for "just this one query." It works, it ships, then it gets copy-pasted three more times and nobody remembers why.
- Unescaped admin content. Someone thinks "only admins see this," so they use {!! !!}. Then the admin account gets compromised.
- Checking .env into staging branches. "Just for testing." Staging gets cloned to production, and secrets leak.
- Disabling CSRF protection locally. Developers disable it to test APIs, then forget to re-enable it before pushing.
Key Takeaways
- Mass assignment: Use $fillable to explicitly whitelist safe fields, or validate and assign manually.
- SQL injection: Trust Eloquent and parameterized queries; never concatenate or interpolate user input into SQL.
- XSS: Use {{ }} for user data by default; reserve {!! !!} for sanitized or trusted HTML only.
- Secrets: Keep .env out of Git, use a vault for production, and rotate keys immediately if exposed.
- Code review: Watch for these four patterns during review—they're the most common security issues we find in Laravel applications.






Comments
Loading comments…