Web application security in Laravel: from OWASP to production operations
Most web application security failures are not sophisticated. They are configuration oversights, missing middleware, unescaped output, or secrets committed to version control. The pattern is consistent across Laravel applications of every size: the gap between tutorial-grade code and production-grade security is where breaches happen. Reducing the attack surface is not a one-off task; it is a continuous discipline that spans code, configuration, and operations.
This page covers how we approach web application security across the full stack. Not just the code, but the deployment pipeline, the monitoring, the backups, and the operational discipline that keeps systems safe after launch day. If you are inheriting an application from another team, the security assessment is where we start.
Why most web applications ship with known vulnerabilities
A fresh Laravel installation handles a surprising amount of security out of the box. CSRF tokens on every form. Bcrypt password hashing. Parameterised queries through Eloquent. Blade template escaping. These defaults cover the basics, and many developers stop there.
The problem is that defaults protect against the common case, not your specific case. A raw database query bypasses Eloquent's parameterisation. An {!! $html !!} directive in Blade disables escaping. A misconfigured CORS policy opens the API to cross-origin abuse. Every Laravel project we audit has at least one place where a developer stepped outside the framework's safety rails without adding compensating controls.
The distinction matters: The naive approach treats security as a checklist run once before launch. The production approach treats it as a property of the architecture, enforced at every layer, tested continuously, and monitored in production.
How Laravel handles the OWASP Top 10
The OWASP Top 10 catalogues the most critical web application security risks. Laravel addresses most of them through framework conventions, but each requires deliberate application. The principle of defence in depth applies throughout: no single control is sufficient, and every layer assumes the layer above it has failed.
The first three categories account for the majority of vulnerabilities we find in inherited codebases. They are all application-level concerns where the framework provides strong defaults that developers routinely bypass.
A01: Broken access control
Laravel provides Gates and Policies for authorisation. The failure mode we see most often: developers check permissions in the controller but not in the query scope. A user modifies a URL parameter, and the application returns another customer's data. The fix is to scope every query to the authenticated user's permissions at the database layer, not just the route layer.
A02: Cryptographic failures
Laravel uses Bcrypt or Argon2id for password hashing and AES-256-CBC for encryption via the Crypt facade. The common mistake is storing sensitive data (API keys, personal identifiers) in plain text in the database "because it's behind authentication." Encryption at rest is a separate concern from access control.
A03: Injection
Eloquent parameterises queries by default. The risk appears with DB::raw(), whereRaw(), and raw expressions in order clauses. We flag every raw query in code review and require explicit parameterisation: DB::select('SELECT * FROM users WHERE email = ?', [$email]), never string concatenation.
A04: Insecure design
This is architectural, not code-level. Rate limiting on authentication endpoints, account lockout after failed attempts, and separation of privilege between user roles following the principle of least privilege. Laravel's ThrottleRequests middleware handles the first case. The other two require deliberate design decisions during data modelling and role definition.
A05: Security misconfiguration
APP_DEBUG=true in production is the classic Laravel example. We have encountered this on client applications inherited from other agencies, twice in the last three years. Debug mode exposes environment variables, database credentials, and full stack traces to anyone who triggers an error. Our deployment pipeline checks for this automatically.
A06: Vulnerable and outdated components
Composer and npm dependency trees grow quickly. composer audit and npm audit run in our CI pipeline on every push. We review Dependabot alerts weekly and apply security patches within 48 hours of disclosure.
The remaining four categories shift from code-level controls to operational and supply chain concerns. These are where security and operations become a single discipline.
A07: Identification and authentication failures
Covered in detail in the authentication patterns section below. Rate limiting, session management, and multi-factor authentication are the critical controls.
A08: Software and data integrity failures
This covers CI/CD pipeline security, unsigned deployments, and supply chain attacks such as dependency confusion. We pin dependency versions, verify package checksums, and deploy from a single, audited pipeline. No manual server edits.
A09: Security logging and monitoring failures
Covered below under monitoring and operations. Without logging, breaches go undetected until the damage is visible to customers.
A10: Server-side request forgery (SSRF)
Any feature that fetches a user-supplied URL (webhooks, image imports, URL previews) is an SSRF vector. We validate URLs against an allowlist of schemes and hosts, block internal IP ranges (169.254.x.x, 10.x.x.x, 127.x.x.x), and run outbound requests through a proxy where possible.
Authentication and authorisation patterns that survive production
Authentication is identity: proving who someone is. Authorisation is permission: controlling what they can do. Conflating the two is the most common architectural mistake we fix in inherited codebases.
Authentication
Laravel ships with Sanctum for SPA and mobile token authentication, and supports Passport for full OAuth 2.0 flows. For most business applications, Sanctum with session-based authentication is sufficient and simpler to reason about.
Password policy matters more than password complexity. We enforce minimum length (12 characters), check against the HaveIBeenPwned breached password database using Laravel's Password::uncompromised() rule, and implement progressive delays after failed login attempts. TOTP-based multi-factor authentication (using packages like pragmarx/google2fa-laravel) is standard on any application handling financial or personal data.
Session management
HTTP-only cookies prevent JavaScript access to session tokens. The secure flag ensures cookies transmit only over HTTPS. SameSite=Lax prevents cross-site request attachment. We rotate session IDs after login with session()->regenerate() and enforce absolute session timeouts, not just idle timeouts.
Resource-level authorisation
Instead of fetching a record and then checking ownership, the query includes the ownership constraint: $user->orders()->findOrFail($orderId) rather than Order::findOrFail($orderId) followed by a manual check. This eliminates an entire class of insecure direct object reference (IDOR) vulnerabilities.
Authorisation
Laravel's Gate and Policy system is well designed but under-used. We define policies for every Eloquent model and register them in AuthServiceProvider. The critical pattern: always authorise at both the route level (middleware) and the query level (scoped queries). Route-level authorisation prevents access to endpoints. Query-level authorisation prevents data leakage through parameter manipulation.
In multi-tenant applications, query scoping becomes even more critical. Every database query, cache key, queued job, and file upload must be scoped to the correct tenant. The failure is silent: the application works perfectly until a user sees another tenant's data. Global query scopes applied at the model level are the baseline control, but they can be bypassed by certain Eloquent query patterns, which is why we test for tenant leakage explicitly in our integration test suites.
Defending against injection, XSS, and CSRF in practice
These three attack vectors account for the majority of web application vulnerabilities. Laravel provides strong defaults for each, but defaults only work when developers stay within the framework's conventions. Every API integration and third-party data source introduces additional input surfaces that need the same level of validation.
| Attack vector | Where the risk appears | Our mitigation |
|---|---|---|
| SQL injection | Raw queries, orderByRaw(), dynamic column names. Any Raw method call bypasses parameterisation. |
Static analysis rule flags every Raw method call for mandatory review. Parameter binding without exception where raw SQL is genuinely necessary. |
| Cross-site scripting (XSS) | Unescaped {!! !!} syntax in Blade templates, typically for WYSIWYG editor output. |
Every use of unescaped output requires sanitisation through HTMLPurifier before storage. Content Security Policy headers restrict script sources as defence in depth. |
| CSRF | Adding API routes to the CSRF exclusion list when those routes still use session authentication. | CSRF protection automatic for web routes. API routes using token authentication (Sanctum, Passport) excluded correctly because they do not use cookies. |
Security headers
We configure security headers at the web server level to provide client-side defence in depth. Every application we deploy scores A+ or higher on the HTTP Observatory.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadenforces HTTPSContent-Security-Policyrestricts script, style, and connection sourcesX-Content-Type-Options: nosniffprevents MIME-type sniffingX-Frame-Options: DENYblocks clickjackingReferrer-Policy: strict-origin-when-cross-originlimits referrer leakagePermissions-Policydisables unused browser features (camera, microphone, geolocation)
Secrets, dependencies, and the software supply chain
Environment variables are Laravel's primary secrets mechanism through .env files. The rules are non-negotiable: .env is in .gitignore, never committed to version control. Production secrets are injected through the hosting environment or a secrets manager (AWS Secrets Manager, HashiCorp Vault), never baked into container images or deployment artefacts.
If it has touched Git, it is compromised. We have inherited three separate codebases where .env files containing database credentials and API keys were committed to Git history. Removing secrets from Git requires rewriting history with git filter-branch or BFG Repo-Cleaner, then rotating every exposed credential. It is a painful exercise that nobody wants to repeat.
Dependency security is a software supply chain problem. A Laravel application with 80 Composer packages and 200 npm packages carries a large attack surface composed mostly of code nobody on the team wrote. We manage this through automated scanning in CI, Dependabot or Renovate for update pull requests, lock file integrity verification, weekly security advisory reviews, and version pinning for production deployments. The Log4Shell vulnerability in 2021 demonstrated that a single transitive dependency can compromise an entire fleet.
Deployment pipelines and zero-downtime releases
Security and operations converge at the deployment pipeline. A manual deployment process, where someone SSHes into a server and runs git pull, is both a security risk and an operational liability. Every manual step is an opportunity for inconsistency, and every SSH session is an attack surface.
Our standard deployment pipeline uses GitHub Actions or GitLab CI with four stages.
Zero-downtime deployment means the application serves requests continuously during release. The old version handles requests until the new version is fully ready, then a symlink swap makes the transition atomic. If the new version fails health checks, the symlink reverts. No maintenance windows, no "please try again in five minutes." Database migrations run as part of this pipeline, with careful attention to backwards compatibility so the old application version can still serve requests while the migration executes.
Infrastructure is defined as code (Terraform, Ansible) and version-controlled alongside the application. Server configuration changes go through the same review process as application code. This eliminates configuration drift, where production slowly diverges from what anyone thinks it looks like, and provides a complete audit trail of every infrastructure change. The approach is sometimes called DevSecOps: treating security, development, and operations as a single integrated discipline rather than separate handoffs.
SSH access to production servers is restricted to key-based authentication only, limited to a small set of IP addresses, and logged. For most operational tasks (restarting queues, clearing caches, running migrations), we use the CI pipeline rather than direct server access. The goal is that nobody needs to SSH into production during normal operations.
Monitoring, backups, and disaster recovery
Security does not end at deployment. An application without monitoring is an application where breaches go undetected.
Error tracking
Sentry or Bugsnag captures every unhandled exception with full context: request parameters, authenticated user, stack trace, and the git commit that produced the error. We configure alerts for error rate spikes, not individual errors, to maintain signal quality. A sudden increase in 403 responses might indicate an access control probe. A spike in 500 errors after deployment triggers automatic investigation.
Uptime and performance monitoring
External services (Pingdom, UptimeRobot) check from multiple geographic locations every 60 seconds. Downtime alerts go to Slack and an on-call phone number. Application performance monitoring tracks database query times, queue throughput, and memory consumption. Slow queries and background job backlogs are leading indicators of problems that affect availability before they affect users.
Backups
Backups follow the 3-2-1 rule: three copies, two different storage types, one offsite. Database backups run hourly with 48-hour retention for hourly snapshots, daily for 30 days, weekly for 12 months. File storage (uploads, generated documents) replicates to a separate region.
The critical discipline is testing restores. We run a full database restore to a staging environment monthly. A backup that has never been restored is not a backup; it is a hope.
Disaster recovery
Recovery requires defined objectives. Recovery Time Objective (RTO) is how quickly the system must be back online. Recovery Point Objective (RPO) is how much data loss is acceptable. For most business applications we build, the targets are RTO under four hours and RPO under one hour. Infrastructure-as-code makes recovery reproducible: spin up new infrastructure, restore the latest backup, update DNS. We document the procedure, and we practice it.
Incident response and UK breach notification
Preparation is the difference between a controlled response and a panic. Most incident response content focuses on what to do during a breach. The more important work happens before one occurs.
Before an incident
Every application we maintain has an incident response document stored offline (not only in the application itself). It contains the system inventory, escalation contacts with out-of-hours phone numbers, hosting provider support channels, a communications approval chain, and a decision tree for the first 60 minutes. This document exists so that the person who discovers the incident at 2am does not have to improvise.
The 72-hour clock
Under GDPR Article 33, organisations must notify the ICO within 72 hours of becoming aware of a personal data breach. The clock starts at discovery, not at the time the breach occurred. This means monitoring and alerting directly affect your compliance window: if you detect a breach 48 hours after it happens, you have already consumed two thirds of your notification period before you start investigating.
Not just data theft: Availability and integrity incidents can be reportable under GDPR Article 33, not only confidentiality breaches. If a system outage prevents access to personal data for an extended period, or if data is corrupted, the ICO expects you to assess whether notification is required.
After an incident
Post-incident review is mandatory, not optional. We document what happened, how it was detected, what the response timeline looked like, what worked, what did not, and what changes will prevent recurrence. The breach log records every action taken and every decision made, which is exactly what the ICO expects to see if they investigate. This feeds back into the monitoring configuration, the deployment pipeline, and the incident response document itself.
The first 10 fixes in an inherited Laravel application
When we take over an existing application, the security assessment follows a consistent triage order. These are the ten items we check first, ranked by the ratio of risk reduction to effort. Most take minutes to verify and hours to fix if they fail.
- Check
APP_DEBUGin production. If it istrue, every error exposes environment variables, database credentials, and full stack traces. This is the single highest-risk, lowest-effort fix. - Search Git history for
.envcommits. Rungit log --all --diff-filter=A -- .env. If the file was ever committed, every secret in that file should be considered compromised and rotated. - Verify HTTPS enforcement. Check for HSTS headers and confirm the application redirects HTTP to HTTPS. Test with the HTTP Observatory.
- Audit route-only permission checks. Look for controllers that call
authorize()or check gates but do not scope database queries to the authenticated user. This is where IDOR vulnerabilities live. - Run
composer auditandnpm audit. Known vulnerabilities in dependencies are the fastest path to exploitation after configuration errors. - Check the deployment method. If deployment involves SSH and
git pullon a production server, there is no audit trail, no rollback mechanism, and likely shared credentials. - Verify backup existence and last test date. Ask for the most recent restore test. If nobody can provide a date, the backups are unverified.
- Review CORS and CSRF configuration. Check
cors.phpfor overly permissive origins and the CSRF exclusion list for routes that still use session authentication. - Check for mass assignment protection. Laravel's
$fillableand$guardedproperties are the defence against mass assignment attacks. Verify every model defines one or the other explicitly. - Review error handling and logging. Confirm that exceptions are captured (Sentry, Bugsnag) and that the application does not return detailed error information to end users in production.
This triage typically takes half a day. It does not replace a full security audit, but it identifies the issues most likely to cause real damage and feeds into the ongoing maintenance plan.
What this looks like in practice
Web application security is not a feature you add before launch. It is an operational discipline that runs from the first line of code through every deployment and into ongoing maintenance.
These patterns belong in every application from day one. They are the habits that prevent 3am phone calls.
-
Preventive investment over reactive cost The cost of getting security right is a fraction of the cost of getting it wrong. A breached application means regulatory notification, customer trust damage, and weeks of forensic investigation.
-
GDPR compliance built in GDPR requires reporting breaches within 72 hours. The preventive investment is measured in hours of configuration. The reactive cost is measured in months and reputation.
-
Tested backups with verified restoration Not "we think we can restore" but "we did restore on this date and it took 47 minutes."
-
Proactive monitoring with actionable alerts Problems detected before users report them. Alerts that require response, not dismissal.
Secure your applications
If you are running a web application and you are not confident in its security posture, or if you are planning a new build and want production-grade security from the start, we are happy to talk it through. For applications already in production, security is covered by our ongoing support service. If you have inherited an application from another team, the security triage described above is where we start.
Discuss your security posture →