Laravel is one of the most popular PHP frameworks, used by millions of developers worldwide. Its expressive syntax and built-in security features make it a solid foundation — but many teams leave critical default configurations in place or misconfigure key components. Here are the most important security practices to apply to every Laravel application.
1. Protect Your .env File
The .env file contains your database credentials, API keys, and application secrets. It must never be publicly accessible. If your web server is misconfigured, https://yoursite.com/.env returns the file contents directly — this is one of the most commonly exploited misconfigurations in Laravel deployments.
# Nginx — deny access to .env
location ~ /\. {
deny all;
}
# Apache .htaccess
<Files .env>
Order allow,deny
Deny from all
</Files>
Always add .env to your .gitignore. Commit only a .env.example with placeholder values. Never hardcode secrets in source files.
2. Set APP_DEBUG = false in Production
When APP_DEBUG=true, Laravel renders full exception pages with stack traces, SQL queries, environment variables, and file paths. This information is invaluable to an attacker mapping your application internals.
# .env (production)
APP_ENV=production
APP_DEBUG=false
Use a dedicated exception tracking service like Sentry or Bugsnag to capture errors without exposing them to users. Configure a generic 500 error view in resources/views/errors/500.blade.php.
3. Mass Assignment Protection
Mass assignment vulnerabilities occur when user-supplied data is passed directly to Model::create() or Model::update() without filtering. An attacker can add unexpected fields — like is_admin or role — to the request and have them written to the database.
// Vulnerable
User::create($request->all());
// Safe — use $fillable to whitelist
class User extends Model {
protected $fillable = ['name', 'email', 'password'];
// Or use $guarded to blacklist (less safe by default):
// protected $guarded = ['is_admin', 'role'];
}
// Even safer — use validated data from Form Request
User::create($request->validated());
Prefer $fillable over $guarded. Whitelisting is safer than blacklisting — if you add a new sensitive column, $guarded requires you to remember to update it, while $fillable blocks it automatically.
4. SQL Injection via Raw Queries
Laravel's Eloquent ORM and Query Builder use PDO parameterized queries by default and are safe from SQL injection. The risk arises with DB::select(), whereRaw(), orderByRaw(), and similar methods that accept raw SQL.
// Vulnerable
$results = DB::select("SELECT * FROM users WHERE email = '$email'");
// Safe — use bindings
$results = DB::select("SELECT * FROM users WHERE email = ?", [$email]);
// Also safe — named bindings
$results = DB::select(
"SELECT * FROM users WHERE email = :email",
['email' => $email]
);
Audit all whereRaw, selectRaw, orderByRaw, and DB::statement calls in your codebase. Every one of them that incorporates user input is a potential injection point.
5. XSS and Blade Templating
Laravel's Blade templating engine automatically escapes output when you use the {{ }} double-brace syntax, preventing reflected XSS. The danger is the unescaped output syntax {!! !!}, which renders HTML as-is.
{{-- Safe — output is HTML-escaped --}}
{{ $userInput }}
{{-- Dangerous — only use with trusted, sanitized content --}}
{!! $userContent !!}
Use {!! !!} only when rendering HTML you fully control — such as processed Markdown or content from a WYSIWYG editor that has been sanitized server-side with a library like HTMLPurifier or DOMPurify. Never pass raw user input to unescaped output.
6. CSRF Protection
Laravel includes CSRF protection via the VerifyCsrfToken middleware, enabled by default for all web routes. Every form POST must include the CSRF token.
<!-- Blade form -->
<form method="POST" action="/profile">
@csrf
...
</form>
// For AJAX requests — include in meta tag and read via JS
<meta name="csrf-token" content="{{ csrf_token() }}">
Never add routes to the $except array in VerifyCsrfToken without a clear security justification. API routes that use Sanctum token authentication are legitimately exempt — but all browser-session routes must be protected.
7. Secure File Uploads
File uploads are a common attack vector. An attacker who can upload a PHP file to a web-accessible directory and execute it has full code execution on your server.
// Validate file type by MIME, not extension
$request->validate([
'avatar' => 'required|file|mimes:jpg,png,gif|max:2048',
]);
// Store in non-public disk (not public/)
$path = $request->file('avatar')->store('avatars', 'private');
// Serve via controller, not direct URL
return Storage::disk('private')->response($path);
Never store uploaded files in the public/ directory unless you are certain they cannot be executed. Configure your web server to deny PHP execution in upload directories as a defense-in-depth measure.
8. Remove Debug Tools from Production
Laravel Telescope and Debugbar are invaluable in development — in production, they expose request data, query logs, mail content, and exception details to anyone who can access /telescope or triggers the debugbar.
// AppServiceProvider.php
public function register(): void {
if ($this->app->environment('local', 'staging')) {
$this->app->register(TelescopeServiceProvider::class);
}
}
// Or in config/telescope.php:
'enabled' => env('TELESCOPE_ENABLED', false),
If you need Telescope in production for debugging, restrict access to specific IP addresses or authenticated admin users via the gate configuration.
9. Session Security
Configure session cookies to be transmitted only over HTTPS and inaccessible to JavaScript. These settings are in config/session.php or overridden via environment variables.
# .env
SESSION_SECURE_COOKIE=true # HTTPS only
SESSION_HTTP_ONLY=true # Block JS access (default)
SESSION_SAME_SITE=strict # Prevent CSRF via cookie
10. Rate Limiting on Authentication Routes
Without rate limiting, login endpoints are vulnerable to brute force and credential stuffing attacks. Laravel includes a built-in rate limiter configurable per route or route group.
// routes/web.php
Route::middleware(['throttle:login'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/password/email', [PasswordController::class, 'sendLink']);
});
// RateLimitServiceProvider or bootstrap/app.php
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Laravel Breeze and Jetstream include rate limiting out of the box. If you are building a custom auth flow, add it explicitly. Five attempts per minute per IP is a reasonable starting point for login endpoints.
Automated Security Scanning
These manual checks catch configuration-level issues. An automated scanner like Shieldome checks runtime behavior — security headers, TLS configuration, exposed paths, cookie flags, and CORS policy — from the attacker's perspective, without requiring code access. Run a scan against your staging environment before every significant deployment.