Laravel Security Audit Checklist - 47 Checks for Production Apps
A thorough security audit checklist covering every layer of a Laravel application. Authentication, authorization, input validation, database security, API protection, file handling, session management, encryption, logging, and infrastructure.
Authentication & Password Security
Verify config/hashing.php uses bcrypt or argon2id as the driver. Never use MD5 or SHA-256 for password storage. Laravel defaults to bcrypt, but confirm no one has changed it. Check that existing passwords in the database start with $2y$ (bcrypt) or $argon2id$ (argon2).
Check your validation rules on registration and password change forms. The NIST recommendation is a minimum of 8 characters, but 12+ provides a stronger security margin. Update Password::min() or your custom validation rules accordingly.
Verify that your login route applies throttling. In Laravel Breeze/Fortify, check the LoginRequest class or RateLimiter::for() definitions in RouteServiceProvider. Aim for no more than 5 attempts per minute per IP/email combination.
Check config/auth.php under passwords.users.expire. The default is 60 minutes. For high-security applications, reduce to 15-30 minutes. Expired tokens should be pruned regularly via the auth:clear-resets command.
Admin accounts are high-value targets. Verify that TOTP-based MFA (Google Authenticator, Authy) or WebAuthn is available and enforced for all users with administrative privileges. Packages like laravel-fortify provide built-in MFA support.
When a user changes their password, all existing remember tokens should be invalidated. Verify that your password change flow calls the user's forceFill method to update the remember_token column or uses Auth::logoutOtherDevices().
Authorization & Access Control
Run php artisan route:list and inspect every route. Look for routes missing auth middleware that should require login. Check for routes that accept user IDs or resource identifiers without verifying the authenticated user owns that resource.
Each Eloquent model that represents a user-owned resource should have a corresponding Policy. Run php artisan make:policy if missing. Verify that controllers call $this->authorize() or use middleware('can:') on each action.
Admin routes should not rely solely on a role check in the UI. Apply a dedicated middleware or Gate::define() that checks for admin privileges server-side. Group admin routes with a prefix and middleware stack that includes the admin authorization gate.
If using Sanctum, verify that tokens are created with specific abilities like ['read-posts', 'create-posts'] rather than ['*']. Audit existing tokens in the personal_access_tokens table for wildcard permissions. Revoke and reissue with scoped abilities.
Every Eloquent model should define a $fillable array listing only the fields users are allowed to modify. Review each model and ensure sensitive fields (role, is_admin, email_verified_at, balance) are never in $fillable. Avoid using Model::unguard() in production code.
Input Validation & XSS
Every controller method that accepts user input should use a dedicated Form Request class with explicit validation rules. Check for controllers that call $request->all() or $request->input() without validation. Missing validation is a common source of injection vulnerabilities.
Search your Blade templates for {!! !!} usage. Each instance should be reviewed to confirm it never outputs user-supplied content. The {{ }} syntax auto-escapes HTML entities. If you must render HTML, sanitize it server-side before storing.
Validate uploaded files with rules like mimes:pdf,jpg,png, max:10240, and file. Do not rely on the client-provided MIME type alone. Use the file's actual content to determine type. Reject executables (.php, .sh, .exe) explicitly.
If your application accepts HTML input (WYSIWYG editors, markdown with HTML), sanitize it with a library like HTMLPurifier or Bleach. Define a strict allowlist of tags and attributes. Strip event handlers (onclick, onerror) and javascript: URLs.
When accepting JSON payloads, validate each field using Laravel's array validation rules (e.g., 'items.*.id' => 'required|integer'). Never use json_decode() and pass the result directly to a model without validation. This prevents type confusion and injection.
SQL Injection & Database
Search your codebase for DB::select, DB::statement, and any string concatenation near query building. All user input must pass through parameter bindings (? placeholders or named bindings). Raw concatenation like "WHERE id = " . $id is vulnerable to SQL injection.
Audit every usage of DB::raw(), selectRaw(), whereRaw(), and orderByRaw(). If any user-supplied value appears inside the raw string via variable interpolation or concatenation, refactor to use bindings. Example: whereRaw('score > ?', [$threshold]) is safe.
Search your git history for database credentials using git log -p --all -S 'DB_PASSWORD'. Ensure .env is in .gitignore. If credentials were ever committed, rotate them immediately. Check config/database.php references env() rather than hardcoded values.
The database user your application connects with should only have SELECT, INSERT, UPDATE, DELETE, and CREATE TEMPORARY TABLES privileges. It should never have DROP, ALTER, GRANT, or FILE privileges. Use a separate user for migrations with broader permissions.
Review your migration and seeder files for hardcoded passwords, API keys, or real user data. Seeders should use fake data factories for development. If you need default admin accounts, store the credentials in .env and reference them from the seeder.
CSRF & Session Security
Verify that the VerifyCsrfToken middleware is in the web middleware group and has not been disabled. Check the $except array for any routes that should not be excluded. Every form should include @csrf. AJAX requests should send the X-CSRF-TOKEN header.
Check SESSION_DRIVER in .env. File-based sessions break in multi-server environments because sessions are stored locally on one server. Use database or Redis for shared session storage. For single-server deployments, file is acceptable.
Check SESSION_LIFETIME in .env (in minutes). The default is 120 minutes. For sensitive applications, reduce to 30-60 minutes. Ensure expire_on_close is considered for your use case. Never set the lifetime to an extremely high value.
Check config/session.php for secure (should be true or env-conditional) and http_only (should be true). The secure flag prevents cookies from being sent over HTTP. The http_only flag prevents JavaScript from accessing the session cookie.
Check the same_site option in config/session.php. Lax is the recommended default as it prevents CSRF from cross-origin navigation while allowing normal link clicks. Strict provides more protection but can break flows from external links.
API Security
Review routes/api.php and ensure all endpoints that access user data or perform actions are wrapped in auth:sanctum or equivalent middleware. Public endpoints should be explicitly identified and limited to read-only data that is genuinely public.
Check RouteServiceProvider for RateLimiter::for('api') configuration. The default is 60 requests per minute. Adjust based on your application needs. Consider stricter limits for authentication endpoints and more generous limits for read-only resources.
With APP_DEBUG=false, verify that 500 errors return a generic message. Test by intentionally causing an error in production. Check custom exception handlers to ensure they do not expose database queries, file paths, or class names in API responses.
Review config/cors.php and ensure allowed_origins is not set to ['*'] in production. List only your known frontend domains. Check allowed_methods and allowed_headers are restricted to what your API actually needs.
Verify that your API uses URL-based (/api/v1/) or header-based versioning. Ensure deprecated endpoints return appropriate warnings. Breaking changes should only occur in new versions. Document the deprecation and sunset timeline for old versions.
File & Storage Security
Check your filesystem configuration in config/filesystems.php. User-uploaded files should use the local disk with a root outside public/, or use S3/cloud storage. Serve files through a controller that checks authorization rather than direct public URLs.
Using the original filename risks path traversal attacks (../../etc/passwd) and overwrites. Generate random names with Str::uuid() or use the hashName() method on uploaded files. Store the original name in the database for display purposes only.
Review the links array in config/filesystems.php. The default symlink connects storage/app/public to public/storage. Ensure no additional symlinks expose sensitive directories. Run ls -la public/ to verify what is actually linked.
Check for temporary file creation in your application (PDF generation, CSV exports, image processing). Schedule a cleanup command to remove files older than 24 hours from temporary directories. Use Laravel's scheduler to run this daily.
Test by requesting your-domain.com/.env, /.git/config, /.git/HEAD, and /composer.json in a browser. All should return 404. Configure your web server (Nginx or Apache) to block access to dotfiles and sensitive project files.
Encryption & Secrets
Each environment (local, staging, production) must have its own APP_KEY. Search your repository history for any committed APP_KEY values. If found, generate new keys for all environments immediately with php artisan key:generate.
Identify fields that store sensitive data (SSNs, financial details, personal documents). These should be encrypted using Laravel's encrypt() helper or the $casts array with 'encrypted' cast. Verify the data is unreadable in the database directly.
Search your codebase for hardcoded API keys, tokens, and secrets using patterns like sk_, pk_, key_, secret, token. All should reference env() or config(). Check third-party service configurations in config/services.php reference environment variables.
If using spatie/laravel-backup or similar, enable encryption in the backup configuration. Unencrypted backups stored offsite (S3, Dropbox) are a liability if the storage account is compromised. Verify encryption is working by inspecting a backup file.
Check your HTTP client configuration (Guzzle, Laravel HTTP facade). Set verify to true (never false) and configure minimum TLS version. In config/http.php or client options, specify CURL_SSLVERSION_TLSv1_2 as the minimum. Audit any curl_setopt calls.
Logging & Monitoring
Verify that failed authentication attempts create a log entry including the attempted email, source IP, user agent, and timestamp. Use Laravel's event system to listen for Illuminate\Auth\Events\Failed. This data is essential for detecting brute-force attacks.
Review your logging configuration and any custom log formatters. Check that request logging middleware redacts sensitive fields. Search your log files for patterns like password, token, card_number, cvv. Laravel hides passwords by default in exception reports but custom logging may not.
Check LOG_CHANNEL in .env. Use the daily channel for file-based logging, which creates a new file each day and keeps a configurable number of days (default 14). For production, consider shipping logs to an external service like Papertrail, Datadog, or CloudWatch.
Verify that unhandled exceptions and errors are reported to an alerting service. Check app/Exceptions/Handler.php or the reportable() method for integration. Test by throwing an intentional exception and confirming the alert arrives within minutes.
Create a dedicated security log channel for events like privilege escalation, admin actions, password changes, and MFA changes. This makes it easier to audit security events without sifting through application noise. Configure in config/logging.php.
Set up external monitoring that regularly scans your production environment for common misconfigurations. Tools like StackShield check for exposed .env files, debug mode, missing security headers, open database ports, and certificate expiration without accessing your codebase.
Frequently Asked Questions
How often should I run a Laravel security audit?
Quarterly at minimum, plus after any major release, framework upgrade, or security incident. Automated scanning should run continuously between manual audits.
What is the most commonly missed item in Laravel security audits?
Mass assignment protection. Many developers add fields to $fillable during development and forget to review them before shipping. A single unguarded field can let attackers modify roles, permissions, or billing data.
Can I automate parts of this checklist?
Yes. Static analysis tools like Larastan and PHPStan catch some code-level issues. Dependency scanning (composer audit) catches known vulnerabilities. External monitoring tools like StackShield check for exposed debug mode, missing headers, and server misconfigurations. The manual items focus on business logic and access control that automated tools cannot evaluate.
Should I hire a penetration tester or use this checklist?
Both. This checklist covers known vulnerability patterns and common misconfigurations. A penetration tester finds application-specific issues, business logic flaws, and chained vulnerabilities that no checklist can cover. Use the checklist for regular self-audits and bring in a pentester annually.
What should I prioritize if I can only fix a few things?
Focus on the items that expose data to unauthenticated users: debug mode, exposed .env files, missing authentication on routes, and SQL injection. These have the highest severity because they require zero authentication to exploit.
Related Fix Guides
Laravel .env File Exposed: How to Block Public Access and Rotate Leaked Credentials
Your Laravel .env file is publicly accessible, leaking database credentials, APP_KEY, and API keys. Block it in Apache and Nginx, then rotate every compromised secret.
Laravel Debug Mode in Production: How to Disable APP_DEBUG and Stop Leaking Secrets
APP_DEBUG=true in production exposes stack traces, environment variables, and database credentials to anyone who triggers an error. Here is how to disable it safely and verify the fix.
How to Fix Missing Security Headers in Laravel
Your Laravel app is missing critical security headers like CSP, HSTS, and X-Frame-Options. Learn how to add them with middleware.
Other Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
Laravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Laravel Authentication Security Checklist
Harden your Laravel authentication system against brute-force attacks, session hijacking, and credential theft with this security checklist.
Download This Checklist as PDF
Get this checklist delivered to your inbox for easy reference.
Automate These Checks with StackShield
Stop running through checklists manually. StackShield continuously monitors your Laravel application for the security issues that matter most.
Start Free Trial