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

  1. Add the middleware with report-only mode first
  2. Load every page in your application
  3. Submit every form (login, registration, password reset, contact)
  4. Test any JavaScript-heavy features (Livewire components, file uploads, charts)
  5. Check storage/logs/csp.log for violations
  6. Fix violations and update the policy
  7. Switch to enforcement mode
  8. Test everything again

Verify Your Setup