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.

Progress
0 / 47 completed

Authentication & Password Security

Password hashing uses bcrypt or argon2

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).

Password minimum length is 12+ characters

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.

Login throttling is enabled via RateLimiter

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.

Password reset tokens expire within 60 minutes

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.

Multi-factor authentication is available for admin accounts

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.

Remember me tokens are rotated on password change

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

All routes use middleware (auth, can, etc.)

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.

Policies cover every model action (create, update, delete, view)

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 are behind additional authorization gates

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.

API tokens have scoped abilities (not wildcard)

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.

Mass assignment protection is enforced ($fillable or $guarded)

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

All controller inputs use Form Requests with validation rules

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.

Blade templates use {{ }} (escaped) not {!! !!} for user content

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.

File upload validation checks MIME type, extension, and size

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.

Rich text inputs are sanitized with an allowlist (e.g. HTMLPurifier)

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.

JSON API inputs are validated with array rules, not blindly decoded

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

All queries use Eloquent or parameterized bindings (no raw concatenation)

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.

DB::raw() calls use parameter binding, not string interpolation

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.

Database credentials are in .env, not committed to version control

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.

Database user has minimum required privileges (no GRANT ALL)

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.

Migrations do not contain sensitive seed data

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

CSRF middleware is active on all POST/PUT/DELETE web routes

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.

Session driver is database or Redis (not file in load-balanced environments)

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.

Session lifetime is appropriate (not indefinite)

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.

Secure and http_only cookie flags are enabled in production

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.

SameSite cookie attribute is set to Lax or Strict

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

API routes require authentication (Sanctum, Passport, or custom)

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.

Rate limiting is configured for all API endpoints

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.

API responses do not leak internal error details or stack traces

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.

CORS configuration allows only trusted origins

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.

API versioning strategy prevents breaking client integrations

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

Uploaded files are stored outside the public directory

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.

File names are randomized (never use original user filename as-is)

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.

Storage symlink only exposes intended directories

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.

Temporary files are cleaned up on a schedule

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.

.env, .git, and other sensitive files return 404 from the web server

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

APP_KEY is unique per environment and not committed to git

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.

Sensitive data at rest uses encrypt() or the Crypt facade

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.

All external API keys are stored in .env, not hardcoded

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.

Backup files are encrypted before offsite storage

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.

TLS 1.2+ is enforced for all external API connections

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

Failed login attempts are logged with IP and timestamp

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.

Sensitive data is excluded from log files (passwords, tokens, card numbers)

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.

Log files are rotated (daily channel or external service)

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.

Application errors trigger alerts (Sentry, Bugsnag, or similar)

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.

Security-relevant events have dedicated log channels

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.

External monitoring checks for exposed debug mode, open ports, and expired certs

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

Other Checklists

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