Laravel is one of the easier frameworks to add CSP to, thanks to its middleware system and Blade templating. You can have a working nonce-based CSP policy in about 30 minutes — I’ve done it multiple times and it really is that straightforward.
Here are two approaches: using the spatie package (fastest setup) and custom middleware (more control).
Option 1: The spatie/laravel-csp Package
If you want something that works out of the box:
composer require spatie/laravel-csp
php artisan vendor:publish --provider="Spatie\Csp\CspServiceProvider"
This creates a config file at config/csp.php where you can define your policy:
return [
'enabled' => env('CSP_ENABLED', true),
'report_only' => env('CSP_REPORT_ONLY', false),
'directives' => [
'default-src' => ['self'],
'script-src' => ['self'],
'style-src' => ['self', 'unsafe-inline'],
'img-src' => ['self', 'data:', 'https:'],
'font-src' => ['self'],
'connect-src' => ['self'],
'frame-ancestors' => ['none'],
'base-uri' => ['self'],
'form-action' => ['self'],
],
];
The package handles nonce generation automatically and adds nonces to your Blade scripts. It’s well-maintained and has been around for years.
One thing to note: the spatie package supports report-only mode through the config. Set 'report_only' => true in your .env during development to collect violations without breaking anything.
Option 2: Custom Middleware (My Preference)
I prefer rolling my own middleware because it gives me full control and removes a dependency. Here’s the complete setup:
php artisan make:middleware CspMiddleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Str;
class CspMiddleware
{
public function handle($request, Closure $next)
{
$nonce = Str::random(16);
$response = $next($request);
// Different policies for different environments
if (app()->environment('local')) {
$policy = implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self'",
"frame-ancestors 'none'",
]);
} else {
$policy = implode('; ', [
"default-src 'self'",
"script-src 'self' 'nonce-{$nonce}' 'strict-dynamic'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.yoursite.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]);
}
$response->headers->set('Content-Security-Policy', $policy);
$response->headers->set('X-Nonce', $nonce);
return $response;
}
}
The environment check is important. Laravel Livewire, Vite’s HMR, and some debug tools need unsafe-eval in development. Don’t fight it — use a permissive policy locally and enforce strictly in production.
Registering the Middleware
Laravel 11:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\CspMiddleware::class,
]);
})
Laravel 10 and older:
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ... existing middleware
\App\Http\Middleware\CspMiddleware::class,
],
];
Using Nonces in Blade Templates
Every <script> tag needs the nonce. Here’s how to do it cleanly:
Direct Approach
{{-- In your layout file --}}
<script nonce="{{ request()->header('X-Nonce') }}">
console.log('App initialized');
</script>
{{-- External scripts too --}}
<script nonce="{{ request()->header('X-Nonce') }}"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX">
</script>
Blade Component (Cleaner)
Create a reusable component:
php artisan make:component ScriptTag
// app/View/Components/ScriptTag.php
namespace App\View\Components;
use Illuminate\View\Component;
class ScriptTag extends Component
{
public $nonce;
public $src;
public function __construct($src = null)
{
$this->nonce = request()->header('X-Nonce');
$this->src = $src;
}
public function render()
{
return view('components.script-tag');
}
}
{{-- resources/views/components/script-tag.blade.php --}}
<script @if($src) src="{{ $src }}" @endif nonce="{{ $nonce }}">
{{ $slot }}
</script>
Usage:
{{-- Inline script --}}
<x-script-tag>
console.log('Hello from CSP-safe script');
</x-script-tag>
{{-- External script --}}
<x-script-tag src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" />
For Vite + Laravel
If you’re using Laravel Vite (the default in Laravel 10+), your @vite directives already load scripts from your own domain. Just make sure Vite’s manifest script gets a nonce:
{{-- In your layout --}}
<script nonce="{{ request()->header('X-Nonce') }}" src="{{ Vite::asset('resources/js/app.js') }}"></script>
Or use the @viteReactRefresh Blade directive in development, which handles HMR automatically.
Handling Livewire
Livewire makes AJAX calls back to your own server, so connect-src 'self' covers it. Livewire doesn’t load external scripts either, so no additional script-src entries are needed.
However, if you’re using Livewire with third-party components that inject JavaScript (like Livewire FilePond uploads), check your CSP reports after enabling enforcement.
CSRF Token and Meta Tags
Laravel’s CSRF meta tag doesn’t need a nonce since it’s a <meta> tag, not a <script> tag. It works fine with CSP:
<meta name="csrf-token" content="{{ csrf_token() }}">
API Routes
API routes typically don’t need CSP headers since they return JSON, not HTML. You can exclude them:
// In your middleware
public function handle($request, Closure $next)
{
if ($request->is('api/*')) {
return $next($request);
}
// ... CSP logic
}
Or register the middleware only on the web group, which doesn’t apply to API routes by default.
CSP Violation Reporting
Add a dedicated endpoint:
// routes/web.php
Route::post('/csp-report', function (\Illuminate\Http\Request $request) {
$report = json_decode($request->getContent(), true);
$cspReport = $report['csp-report'] ?? [];
\Log::channel('csp')->warning('CSP Violation', [
'page' => $cspReport['document-uri'] ?? 'unknown',
'blocked' => $cspReport['blocked-uri'] ?? 'unknown',
'directive' => $cspReport['violated-directive'] ?? 'unknown',
'source' => $cspReport['source-file'] ?? 'unknown',
'line' => $cspReport['line-number'] ?? null,
]);
return response('', 204);
});
Create a dedicated log channel in config/logging.php:
'channels' => [
'csp' => [
'driver' => 'daily',
'path' => storage_path('logs/csp.log'),
'level' => 'warning',
'days' => 30,
],
],
Now check storage/logs/csp.log after deploying to see what’s being blocked.
Testing Checklist
- Add the middleware with report-only mode first
- Load every page in your application
- Submit every form (login, registration, password reset, contact)
- Test any JavaScript-heavy features (Livewire components, file uploads, charts)
- Check
storage/logs/csp.logfor violations - Fix violations and update the policy
- Switch to enforcement mode
- Test everything again