Laravel security best practices are less about exotic attacks and more about consistently using the protections the framework already gives you. Laravel ships with hashed passwords, CSRF tokens, parameterized queries, and escaped templates out of the box β and yet Laravel applications still get breached, almost always because a developer stepped around a default without realizing what it was protecting. This post is a field checklist of the defenses that matter, why they matter, and the specific mistakes that disable them.
Authentication: lean on the framework, harden the edges
Do not hand-roll authentication. The starter kits, Fortify, and Sanctum are audited, maintained, and handle the details people forget: password hashing with bcrypt or argon2, session regeneration on login, and remember-token rotation. Your job is the configuration around them:

- Rate limit login attempts. Throttle by username plus IP, not IP alone, or an attacker behind a botnet walks straight past it. Fortify does this out of the box; if you build your own controller, use the
RateLimiterfacade. - Enforce a password policy with the Password rule object.
Password::min(12)->uncompromised()checks candidate passwords against the haveibeenpwned database via k-anonymity, which blocks the credential-stuffing dictionary without you storing anything. - Regenerate the session on privilege change. Laravel\'s built-in login does this, but if you ever call
Auth::login()manually, follow it with$request->session()->regenerate()to kill session fixation. - Add two-factor authentication for anything sensitive. Fortify ships TOTP-based 2FA; enabling it is mostly UI work.
Authorization deserves equal attention. Every controller action that touches a model owned by a user should go through a policy β $this->authorize('update', $order) or the can middleware. The most common Laravel vulnerability in the wild is not injection; it is insecure direct object references, where /orders/1523 happily serves someone else\'s order because nobody wrote the policy check.
Validation is a security boundary, not a UX feature
Treat every request as hostile until it has passed a form request. Two habits make validation an actual defense:
class UpdateProfileRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update', $this->route('profile'));
}
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns', 'max:254'],
'website' => ['nullable', 'url:https', 'max:255'],
'age' => ['nullable', 'integer', 'between:13,120'],
];
}
}
First, validate type and shape, not just presence. A required rule alone will happily accept an array where you expected a string, and PHP\'s juggling can turn that into odd behavior downstream β always pair with string, integer, array, or boolean. Second, use only validated data afterwards: $request->validated() or $request->safe()->only([...]), never $request->all(). The unvalidated bag is where mass assignment attacks come from.
Mass assignment: the classic Laravel foot-gun
Every Eloquent model guards against mass assignment by default, and every tutorial promptly disables it. The dangerous pattern looks innocent:
// Model
protected $guarded = [];
// Controller
$user->update($request->all());
With that combination, a request containing is_admin=1 silently promotes the attacker. The fix is layered: prefer an explicit $fillable list on models with sensitive columns, and always feed update() and create() from $request->validated() so only keys you declared rules for get through. In development, add Model::preventSilentlyDiscardingAttributes() inside AppServiceProvider::boot() β it throws instead of silently dropping unfillable attributes, which surfaces mistakes in tests rather than production.
SQL injection: stay inside the query builder
Eloquent and the query builder use PDO parameter binding everywhere, so User::where('email', $email) is safe no matter what $email contains. Injection re-enters through the raw methods:

// Vulnerable: user input interpolated into raw SQL
$users = DB::select("SELECT * FROM users WHERE name LIKE '%{$q}%'");
// Safe: bindings
$users = DB::select('SELECT * FROM users WHERE name LIKE ?', ['%'.$q.'%']);
Audit every DB::raw(), whereRaw(), orderByRaw(), and selectRaw() in your codebase β they are fine with bindings, dangerous with interpolation. The subtle one is column names: bindings only cover values, so a sortable table that passes ?sort=name into orderBy($request->query('sort')) needs an allow-list of permitted columns, because orderBy cannot parameterize an identifier.
XSS: respect the escaping default
Blade\'s {{ $value }} escapes output through htmlspecialchars, which neutralizes stored and reflected XSS in one stroke. The vulnerabilities come from the escape hatches:
{!! $value !!}renders raw HTML. Reserve it for content you generated yourself (rendered markdown from trusted authors, for example), and run user-supplied rich text through an HTML sanitizer with an allow-list before it is ever stored.- Attribute context matters. Escaped output inside an unquoted attribute or inside inline JavaScript can still break out. Pass data to scripts with
Illuminate\Support\Js::from($data)or the@jsdirective instead of hand-building JSON in Blade. - User-supplied URLs deserve a scheme check β
javascript:URLs in anhrefsurvive HTML escaping just fine.
Add a Content-Security-Policy header as a second layer. Even a modest policy that disallows inline scripts converts many would-be XSS bugs into console errors.
CSRF: understand what the token protects
The web middleware group verifies a CSRF token on every POST, PUT, PATCH, and DELETE, and @csrf in your forms supplies it. Two rules keep the protection intact. First, never route state-changing actions through GET β the token is not checked there, and a hostile page can trigger GETs freely. Second, be stingy with exclusions: every URI you add to the validateCsrfTokens(except: [...]) call in bootstrap/app.php is a hole, so it should only ever contain webhook endpoints that authenticate by signature instead (verify Stripe\'s or GitHub\'s HMAC header before trusting the payload). API routes using Sanctum tokens are inherently CSRF-immune because browsers cannot attach the Authorization header cross-site β but cookie-based SPA auth is not, which is why Sanctum\'s SPA mode has its own CSRF cookie handshake.
The unglamorous fundamentals
- APP_DEBUG=false in production. Debug pages leak environment variables, credentials, and query text. This single misconfiguration has caused more real Laravel data exposure than any framework bug.
- Keep APP_KEY secret and stable β it encrypts sessions and cookies. Rotate it deliberately, never commit it.
- Force HTTPS with
URL::forceScheme('https')behind a proxy, set session cookies tosecureandhttp_only, and enable HSTS at the web server. - Update dependencies. Run
composer auditin CI; most framework CVEs are patched within hours, and the only vulnerable apps are the ones that never upgrade. - Validate file uploads by extension allow-list and MIME, store them outside the webroot or on S3, and never trust the client-supplied filename.
Closing thoughts
Laravel\'s security posture in 2026 is genuinely strong β the framework team ships sane defaults and patches fast. Your responsibility is to avoid disabling those defaults casually: keep models guarded, keep raw queries bound, keep Blade escaping on, keep CSRF exclusions empty, and put a policy in front of every resource. Do a quarterly pass with composer audit and a grep for $guarded = [], !!, and Raw(, and you will be ahead of the vast majority of applications on the internet.

Related Service
π» Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React β fast, secure, scalable, and tailored to your business goals.
Explore Web Development →
Reviews & Comments
Reviews are moderated and appear after approval.
No reviews yet β be the first to share your thoughts.
Leave a Review