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.
HTTP security headers are browser directives that instruct browsers how to handle your application's content. They emerged over a decade of responding to increasingly sophisticated web attacks. Clickjacking — embedding sites in hidden iframes to hijack user clicks — led to X-Frame-Options. MIME sniffing attacks, where browsers guess a file's type and execute it as JavaScript, led to X-Content-Type-Options. Man-in-the-middle attacks on mixed HTTP/HTTPS content led to Strict-Transport-Security. Cross-site scripting at industrial scale led to Content-Security-Policy.
The attack vectors these headers address are well-documented and actively exploited. Without HSTS, attackers on shared networks (coffee shops, airports) can strip HTTPS and intercept credentials through SSL stripping. Without CSP, injected scripts from XSS vulnerabilities execute freely in the victim's browser. Without X-Frame-Options, a malicious site can embed your login page in a transparent iframe positioned over an attacker-controlled page, tricking users into entering credentials they think are going to your site.
Laravel ships with no security headers configured by default — every new application starts with a zero score on securityheaders.com. This isn't a Laravel-specific issue; most web frameworks take the same approach, leaving header configuration to the developer. The practical result is that the majority of Laravel applications in production have no browser-level security policies. Adding these headers takes less than 30 minutes and eliminates entire classes of browser-level attacks with no impact on functionality.
The Problem
Missing security headers leave your Laravel application vulnerable to cross-site scripting (XSS), clickjacking, MIME sniffing, and protocol downgrade attacks. Browsers rely on these headers to enforce security policies, and without them your users have no protection from these common attack vectors. Most Laravel applications ship with zero security headers configured.
How to Fix
-
1
Create a security headers middleware
Generate a new middleware:
php artisan make:middleware SecurityHeadersThen add the headers in app/Http/Middleware/SecurityHeaders.php:
<?phpnamespace App\Http\Middleware;use Closure; use Illuminate\Http\Request;class SecurityHeaders { public function handle(Request $request, Closure $next) { $response = $next($request);$response->headers->set('X-Frame-Options', 'SAMEORIGIN'); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-XSS-Protection', '1; mode=block'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); $response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'");return $response; } }
-
2
Register the middleware globally
In Laravel 11+, add to bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->append(\App\Http\Middleware\SecurityHeaders::class); })In Laravel 10 and earlier, add to the $middleware array in app/Http/Kernel.php:
protected $middleware = [ // ... existing middleware \App\Http\Middleware\SecurityHeaders::class, ]; -
3
Customize Content-Security-Policy for your app
The CSP header needs to match your application. If you use external scripts (analytics, Stripe, etc.), add their domains:Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.stripe.com; frame-src https://js.stripe.com;Start with Content-Security-Policy-Report-Only to test without breaking your site, then switch to enforcing mode once verified.
How to Verify
Check your headers with curl:
curl -I https://yourdomain.com
You should see all security headers in the response. You can also use securityheaders.com to scan your site and get a grade. Aim for an A or A+ rating.
Prevention
Include the security headers middleware in your base Laravel project template. Test headers in your CI/CD pipeline by checking response headers after deployment. Use StackShield to continuously monitor that headers remain in place after updates and deployments.
Frequently Asked Questions
Will adding security headers break my application?
The most likely header to cause issues is Content-Security-Policy, which can block inline scripts, external fonts, or third-party widgets. Start with Content-Security-Policy-Report-Only mode to identify violations before enforcing. The other headers (X-Frame-Options, HSTS, etc.) rarely cause issues.
Do I need all of these headers?
At minimum, you should set X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, and Referrer-Policy. Content-Security-Policy provides the strongest protection but requires careful configuration. Each header protects against a different attack vector.
Can I set security headers at the web server level instead?
Yes, you can set headers in Nginx (add_header directive) or Apache (Header set directive) instead of Laravel middleware. Server-level headers apply to all responses including static files, which is an advantage. However, middleware gives you more flexibility to vary headers per route.
What is a CSP nonce and when do I need one?
A nonce is a per-request random value added to your CSP header and to each inline script tag, allowing those specific scripts to run while blocking all other inline scripts. Use nonces when you have legitimate inline scripts that you cannot move to external files. Generate one per request with bin2hex(random_bytes(16)) and include it as both script-src 'nonce-VALUE' in the header and nonce="VALUE" on each script tag.
How do I get an A+ rating on securityheaders.com?
To achieve A+, you need all of: Strict-Transport-Security with includeSubDomains and preload, Content-Security-Policy without unsafe-inline or unsafe-eval, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. The most common obstacle is CSP — removing unsafe-inline requires converting inline scripts and styles to external files or using nonces.
Related Security Terms
Related Guides
Laravel XSS Prevention Guide: Blade Escaping, {!! !!} Risks & CSP Headers
Prevent cross-site scripting in Laravel. Learn when {!! !!} is safe, how to sanitize HTML input, encode output in Blade templates, and add Content Security Policy headers.
Fix CORS Misconfiguration in Laravel: Wildcard Origins, Credentials & config/cors.php
Using Access-Control-Allow-Origin: * with credentials enabled? That lets any site call your API as the logged-in user. Here is how to lock down config/cors.php properly.
How to Fix Weak SSL/TLS Configuration in Laravel
Your SSL/TLS certificate is expired, misconfigured, or using weak protocols. Learn how to fix SSL issues for your Laravel app.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.