Most web application security failures are not sophisticated. They are configuration oversights, missing middleware, unescaped output, or secrets committed to version control. After building and maintaining 50+ Laravel applications since 2005, the pattern is consistent: the gap between tutorial-grade code and production-grade security is where breaches happen.
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.
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.
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. Laravel's ThrottleRequests middleware handles the first case. The other two require deliberate design.
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.
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 dependency confusion attacks. 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 OAuth2 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.
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.
| 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 Mozilla 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 supply chain problem. A Laravel application with 80 Composer packages and 200 npm packages has a large attack surface that is mostly invisible. 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."
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.
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.
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.
The applications we build for clients at IGC carry these patterns from day one. Not because we follow a checklist, but because after two decades and 50+ Laravel applications, these 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, a conversation is the right starting point. The first one is free and comes with no obligation.
Book a discovery call →