Hardening Laravel: Essential Security Headers
Securing a web application isn't just about validating inputs and escaping outputs (though those are critical). It's also about telling the browser how to behave when loading your site.
HTTP Security Headers are instructions sent by the server to the browser. They can prevent Cross-Site Scripting (XSS), Clickjacking, and other attacks.
For a Markdown blog that might render user-provided HTML (rare, but possible) or load external scripts, these are essential.
How to Add Headers in Laravel
The best place to add global headers is in a Middleware.
Create a new middleware:
php artisan make:middleware SecurityHeaders
In app/Http/Middleware/SecurityHeaders.php:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';");
return $response;
}
}
Don't forget to register it in bootstrap/app.php (for Laravel 11) or app/Http/Kernel.php (older versions).
Breakdown of Headers
1. X-Content-Type-Options: nosniff
Prevents the browser from "sniffing" the content type. If you serve a file as text/plain, the browser won't try to execute it as JavaScript, preventing MIME type confusion attacks.
2. X-Frame-Options: DENY
This prevents your site from being loaded in an <iframe> on another site. This kills Clickjacking attempts where an attacker overlays your site with an invisible button.
3. Strict-Transport-Security (HSTS)
(Not included in the code above because it requires HTTPS). If you serve your site over HTTPS (you should), HSTS tells the browser: "Never load this site over HTTP again, automatically upgrade to HTTPS."
In production (e.g., Nginx config), or in middleware if strictly enforced:
if (app()->environment('production')) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
4. Content-Security-Policy (CSP)
The big boss of security headers. It defines exactly where resources (JS, CSS, Images) can be loaded from.
The example provided:
default-src 'self' -> Only load assets from my own domain.
If you use Google Analytics or Fonts, you must whitelist them:
script-src 'self' https://www.google-analytics.com
Warning: Implementing strict CSP represents a high "breakage risk". If you block inline styles or scripts, parts of your frontend stack (like Alpine.js or Tailwind) might need specific configuration (like nonces or hashes).
Middleware vs Web Server Config
Should you do this in PHP (Laravel) or Nginx/Apache?
- Nginx: Faster. Use this for static assets and general rules.
- Laravel Middleware: More flexible. Use this if you need conditional logic (e.g., relax CSP for a specific admin route).
For a simple blog, Middleware is fine and easier to version control with your code.
Testing
Use securityheaders.com to scan your site. Aim for an A+ rating. It’s a badge of honor for any developer blog.