spatie/laravel-csp
Add Content Security Policy (CSP) headers to your Laravel app with easy configuration and preset policies. Control which scripts, styles, images, and connections are allowed, reduce XSS/data exfiltration risk, and support reporting and nonces.
Installation:
composer require spatie/laravel-csp
php artisan vendor:publish --tag=csp-config
This publishes the config/csp.php file with sensible defaults.
Enable CSP Globally:
Register the middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\Spatie\Csp\AddCspHeaders::class);
})
First Use Case:
Start with the Basic preset (already included in the config). This allows scripts, styles, images, and other resources from your own domain and common CDNs.
// config/csp.php
'presets' => [
Spatie\Csp\Presets\Basic::class,
],
Verify CSP Headers:
Check the response headers in your browser's DevTools (Network tab) or via curl -I http://your-app.test. You should see headers like:
Content-Security-Policy: script-src 'self' 'unsafe-inline'; ...
Preset-Based Configuration:
Use built-in presets for common services (e.g., Google Analytics, Stripe, Hotjar). Add them to the presets array in config/csp.php:
'presets' => [
Spatie\Csp\Presets\Basic::class,
Spatie\Csp\Presets\Google::class, // For Google Analytics/Tag Manager
Spatie\Csp\Presets\Stripe::class,
],
Route/Group-Level Overrides: Override presets for specific routes or groups:
Route::middleware([
\Spatie\Csp\AddCspHeaders::class . ':' . \Spatie\Csp\Presets\Basic::class,
])->group(function () {
// Routes with Basic CSP
});
Route::get('/admin', \App\Http\Controllers\AdminController::class)
->middleware(\Spatie\Csp\AddCspHeaders::class . ':' . \Spatie\Csp\Presets\Stripe::class);
Dynamic Directives:
Add global directives (e.g., allow unsafe-inline for development):
'directives' => [
[\Spatie\Csp\Directive::SCRIPT, \Spatie\Csp\Keyword::UNSAFE_INLINE],
],
Nonce Handling for Inline Scripts/Styles:
Use nonces for inline scripts/styles to avoid unsafe-inline:
<script nonce="{{ csp_nonce() }}">
// Inline script
</script>
Ensure nonce_enabled is true in config/csp.php.
Report-Only Mode: Test CSP changes without breaking production:
'report_only_presets' => [
Spatie\Csp\Presets\Basic::class,
],
Configure a reporting endpoint (e.g., Report URI):
'report_uri' => env('CSP_REPORT_URI', 'https://your-report-uri.com/csp-report'),
Blade Integration: Add CSP meta tags to HTML responses:
<head>
@cspMetaTag
</head>
Vite/Laravel Mix:
Disable CSP during hot reloading (add to config/csp.php):
'enabled_while_hot_reloading' => env('CSP_ENABLED_WHILE_HOT_RELOADING', true),
Custom Presets: Create a preset for third-party services not included in the package:
namespace App\Csp;
use Spatie\Csp\Preset;
class CustomServicePreset extends Preset
{
public function directives(): array
{
return [
\Spatie\Csp\Directive::CONNECT_SRC => ['https://custom-service.com'],
\Spatie\Csp\Directive::SCRIPT_SRC => ['https://custom-service.com'],
];
}
}
Register it in config/csp.php:
'presets' => [
\App\Csp\CustomServicePreset::class,
],
Environment-Specific Config: Use environment variables to toggle CSP:
'enabled' => env('CSP_ENABLED', false), // Disable in staging
Debugging CSP:
Use report-only mode to log violations before enforcing:
'report_only_presets' => [\Spatie\Csp\Presets\Basic::class],
'report_only_uri' => env('CSP_REPORT_ONLY_URI'),
Broken Resources:
script-src or connect-src). Use report-only mode to identify violations first.Nonce Mismatches:
Refused to execute inline script if the nonce doesn’t match.nonce_enabled is true and nonces are generated consistently. For Vite, use @vite('resources/js/app.js') with nonces:
<script nonce="{{ csp_nonce() }}">
@vite('resources/js/app.js')
</script>
Hot Reloading Conflicts:
'enabled_while_hot_reloading' => true in config/csp.php.Reporting Endpoint Misconfiguration:
report_uri is misconfigured.Overly Restrictive Policies:
unsafe-inline or unsafe-eval breaks legacy code.unsafe-inline in development:
'directives' => [
[\Spatie\Csp\Directive::SCRIPT, \Spatie\Csp\Keyword::UNSAFE_INLINE],
],
Caching Headers:
Cache-Control: no-cache for CSP headers or increment the policy hash (e.g., append a version query string).Check Headers:
Use curl -I http://your-app.test or browser DevTools (Network tab) to verify CSP headers are applied.
Test in Report-Only Mode:
Enable report_only_presets and monitor violations before enforcing:
'report_only_presets' => [\Spatie\Csp\Presets\Basic::class],
'report_only_uri' => 'https://your-report-uri.com/csp-report',
Browser Console:
Look for CSP violation messages in the Console tab (e.g., Refused to load the script).
Log Violations: Use a logging service (e.g., Sentry, Laravel Log) to process CSP reports:
// config/csp.php
'report_uri' => 'https://your-app.test/csp-report',
Create a route to handle reports:
Route::post('/csp-report', [\App\Http\Controllers\CspReportController::class, 'store']);
Validate Directives: Use the CSP Evaluator to test your policy.
Custom Nonce Generator: Override the default nonce generator for custom logic:
// config/csp.php
'nonce_generator' => \App\Services\CustomNonceGenerator::class,
Dynamic Presets: Load presets dynamically based on user roles or environments:
// app/Providers/AppServiceProvider.php
public function boot()
{
if (app()->environment('production')) {
config(['csp.presets' => [
\Spatie\Csp\Presets\Basic::class,
\Spatie\Csp\Presets\Google::class,
]]);
}
}
Middleware Logic:
Extend AddCspHeaders to conditionally apply CSP:
How can I help you explore Laravel packages today?