How do you secure a Laravel app against SQLi, CSRF, XSS, and privilege abuse?
Laravel Developer
answer
I secure Laravel by defaulting to the Query Builder and Eloquent (prepared statements) to prevent SQL injection, and I never pass unchecked input into DB::raw. CSRF is enforced via the framework middleware, tokens in Blade forms, and Sanctum flows for SPAs. XSS is mitigated with Blade auto-escaping, strict avoidance of {!! !!}, server-side sanitization for rich text, and a tight Content Security Policy. I stop privilege escalation with policies, gates, route model binding with ownership and tenant scopes, deny-by-default middleware, and defense-in-depth rate limits and validation.
Long Answer
Securing a Laravel application means using the framework’s safe-by-default features, then tightening the edges where business logic and multi-tenancy create risk. My approach covers input validation, query safety, CSRF, XSS, session and cookie hygiene, and authorization that prevents privilege escalation without harming usability.
1) SQL injection: prepared by default, raw-by-exception
Laravel’s Eloquent and Query Builder use parameter binding, which protects against SQL injection. I keep all reads and writes in these layers; when raw SQL is unavoidable, I use bindings (DB::select('… where id = ?', [$id])) and whitelist column names instead of interpolating user input. I forbid passing request parameters into DB::raw or dynamic order-by without validation. For search and filters, I validate operators and columns, mapping user choices to a vetted allowlist.
2) Input validation and mass assignment
All entry points use Form Requests with rules, custom messages, and authorization. I reject unknown fields (DTOs or fillable on models) to prevent mass-assignment hijacks. Casts ($casts) enforce types, and I normalize data (trim, lowercase emails) before persistence. Validation runs before policies to reduce load and avoid leaking resource existence.
3) CSRF in server-rendered and SPA flows
For Blade forms, @csrf and the VerifyCsrfToken middleware protect writes. For SPAs on the same domain, I prefer Sanctum’s cookie-based session with CSRF cookie and Axios X-XSRF-TOKEN auto-injection, keeping tokens HttpOnly, Secure, and SameSite=Lax. For third-party webhooks, I add route exceptions but verify request signatures or HMAC timestamps, not just path secrets.
4) XSS: escape-by-default plus sanitization and CSP
Blade’s {{ }} escapes output; I treat {!! !!} as a code smell and only allow it for trusted, sanitized HTML. For user-generated rich text, I sanitize on the server (for example, HTMLPurifier or a vetted library) and store the cleaned version. I avoid injecting untrusted strings into attributes or scripts, and I use e() for manual contexts. A strict Content Security Policy (no inline scripts, nonces or hashes for first-party assets, locked connect-src) blocks many XSS classes and third-party script drift.
5) Authentication and session security
I rely on Laravel’s authentication stack (Fortify/Breeze/Jetstream): hashed passwords (Argon2id or bcrypt with high cost), unique email verification, and optional multi-factor (Time-based One-Time Password/WebAuthn). Cookies are encrypted, HttpOnly, Secure, and SameSite. I rotate session IDs after login and on privilege changes, and I set short lifetimes for sensitive areas. Remember tokens are rotated server-side on logout.
6) Authorization and anti–privilege escalation
I use Gates and Policies with deny-by-default. Route model binding is scoped (for example, {team}/projects/{project}) so ownership is enforced in the binding query, not only after fetching. In multi-tenant apps, I apply global scopes or tenant_id constraints in repositories so IDOR (insecure direct object references) cannot occur. Controllers never trust client-side role flags; they consult policies. Admin-only actions use additional middleware (role checks and fresh session timestamps) to mitigate session fixation and stale elevation.
7) Defense around file uploads and storage
I validate MIME type and size, store on S3 or local storage outside the webroot, and never serve user files through the web server directly without signed routes. For images, I use processors that strip scripts and disallow SVG unless sanitized. Downloads are streamed with authorization checks on every request.
8) Rate limiting, brute-force and abuse controls
Using Laravel’s rate limiter, I throttle login and password reset by user and IP, and I add per-route limits for expensive endpoints. I return 429 with Retry-After, and I log abuse events with correlation IDs. For APIs, I consider rotating app secrets and HMAC signatures where appropriate.
9) Secrets, environment, and headers
Secrets live in environment variables managed by a secrets manager; .env never lands in version control. I set security headers via \Fruitcake\Cors and helmet-style middleware equivalents: X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and strict CSP. HSTS is enabled at the edge, and HTTPS is forced.
10) Logging, auditing, and usability
Every auth action (login, logout, failed attempt), role change, and sensitive update is logged (structured JSON). Errors never echo stack traces in production; exception reports include request IDs to correlate logs and traces. Usability comes from clear validation messages, sane session lifetimes, device lists for token revocation, and “undo” where possible (soft deletes, queued outbox emails) rather than blocking flows with excessive prompts.
This layered plan aligns with Laravel’s strengths: prepared statements and validation stop SQL injection, CSRF middleware protects writes, Blade escaping and CSP limit XSS, and policies with scoped bindings prevent privilege escalation—all while keeping sign-in and form flows smooth.
Table
Common Mistakes
Trusting client role flags and skipping Policies, leading to privilege escalation. Allowing {!! !!} or innerHTML equivalents with user content and no sanitization, causing XSS. Using DB::raw or dynamic order-by with unvalidated parameters, enabling SQL injection. Disabling CSRF middleware to “fix” failing AJAX instead of using tokens or Sanctum. Overbroad guarded/fillable causing mass assignment of role_id or is_admin. Serving uploads directly from the webroot, including unsanitized SVG. Long-lived sessions with no rotation. Logging secrets or stack traces in production. Missing rate limits on login, password reset, or search endpoints.
Sample Answers (Junior / Mid / Senior)
Junior:
“I rely on Eloquent and the Query Builder so inputs are bound, not concatenated, preventing SQL injection. I add @csrf to forms and use Blade’s {{ }} to avoid XSS. Authorization goes through Policies, not just front-end checks.”
Mid:
“I enforce Form Requests for validation and authorization, lock models with fillable, and sanitize rich text. For SPAs I use Sanctum with CSRF cookies and SameSite cookies. Policies plus route model binding ensure ownership, and I throttle login and password reset to slow brute force.”
Senior:
“I design multi-tenant authorization with scoped bindings and deny-by-default Policies. I prohibit raw queries without bindings and validate any dynamic column/order inputs. I deploy strict CSP, Argon2id password hashing, session rotation on privilege change, signed URLs for downloads, and rate limiting on sensitive routes. Secrets live in a vault, and all admin actions are audited.”
Evaluation Criteria
Strong answers show: (1) SQL injection prevention via Eloquent/Builder and validated raw bindings, (2) CSRF protection in Blade and Sanctum SPA flows, (3) XSS protection with Blade escaping, sanitization, and CSP, (4) privilege escalation defenses using Policies, Gates, scoped bindings, and tenant filters, (5) input validation with Form Requests and mass-assignment controls, (6) session and cookie hygiene, rate limiting, secure file handling, and secrets management. Red flags: disabling CSRF middleware, using {!! !!} with user content, trusting client roles, interpolating user input into raw SQL, or serving uploads directly without checks.
Preparation Tips
Build a demo: Blade form with @csrf, Form Request validation, and a Policy-protected resource. Add a multi-tenant route model binding (/teams/{team}/projects/{project}) that scopes by team_id. Implement a sanitized rich-text field and verify Blade never uses {!! !!} for user content. Add a strict CSP (no inline scripts) and confirm pages still work. Create a search endpoint that maps allowed sort fields to a whitelist and uses bindings. Enable Sanctum for an SPA route and verify CSRF cookie behavior. Configure rate limits for login and password reset, rotate session on login, and prove admin actions are audited. Attempt XSS, CSRF, and raw SQL payloads in tests to validate defenses.
Real-world Context
A SaaS team fixed silent IDOR by moving from controller-level checks to scoped route model binding plus Policies; cross-tenant access dropped to zero. A content site suffered stored XSS via rich text; server-side sanitization and a strict CSP eliminated the vector. Another team removed ad-hoc DB::raw sorts and added a whitelist; SQL injection probes vanished in logs. After enabling Sanctum and proper CSRF flow, the SPA stopped making unauthenticated writes, and support tickets fell. Login throttling and Argon2id hashing reduced credential-stuffing impact without harming legitimate users.
Key Takeaways
- Use Eloquent/Builder with bindings; validate any raw SQL.
- Enforce Form Requests, fillable models, and typed casts.
- Keep CSRF middleware on; use Sanctum for SPAs.
- Prevent XSS with Blade escaping, server sanitization, and strict CSP.
- Block privilege escalation via Policies, Gates, and scoped bindings (tenant-aware).
- Secure sessions and cookies; rate limit sensitive endpoints; guard uploads and secrets.
Practice Exercise
Scenario:
You are hardening a multi-tenant Laravel app where users manage projects and upload content. Pen tests found IDOR in project access, stored XSS in rich text, and weak CSRF handling for SPA writes.
Tasks:
- Add scoped route model binding: in RouteServiceProvider, constrain Project bindings by team_id. Update Policies to deny by default and assert ownership.
- Convert controllers to Form Requests that validate fields and authorize per Policy. Lock models with protected $fillable and appropriate $casts.
- Replace {!! $content !!} with sanitized output: sanitize on save (HTMLPurifier or equivalent) and render with {{ $sanitized }}. Add a strict CSP with nonces or hashes and no inline scripts.
- Migrate SPA writes to Sanctum: issue CSRF cookie, ensure Axios sends X-XSRF-TOKEN, and keep cookies HttpOnly, Secure, SameSite=Lax. Remove CSRF exceptions except verified webhooks with HMAC.
- Remove dynamic DB::raw sorts; map user sort keys to an allowlist and use Builder methods.
- Add rate limits for login and password reset. Rotate session IDs on login and on role elevation.
- Write tests that attempt cross-tenant access, stored XSS, CSRF on writes, and raw SQL injection. All must fail safely.
Deliverable:
A hardened Laravel app with verified protection against SQL injection, CSRF, XSS, and privilege escalation, preserving smooth user flows for Blade and SPA clients.

