Laravel Content Security Policy: Configure CSP Without Breaking Your App
Only 22% of Laravel apps have a Content Security Policy. Learn how to implement CSP with spatie/laravel-csp, handle Livewire and Vite nonces, and avoid the mistakes that break production.
Content Security Policy is one of those browser security features that every developer has heard of and very few have actually deployed. StackShield scans show that only 22% of Laravel applications return a Content-Security-Policy header. That means roughly four out of five Laravel apps are missing a layer of defence that can significantly blunt the damage of a cross-site scripting attack.
This post covers what CSP is, why it is harder to implement than it sounds, and how to do it properly for a typical Laravel app running Livewire, Alpine.js, and Vite.
What CSP Is and Why It Matters
A Content Security Policy is an HTTP response header. It tells the browser a list of rules: which domains are allowed to serve scripts, which can serve styles, which can load fonts, and so on. If the page tries to load a resource that violates the policy, the browser refuses to load it and can optionally report the violation to an endpoint you control.
The core security benefit is limiting the blast radius of XSS. If an attacker finds an XSS vulnerability in your app and injects a <script> tag pointing to their server, a proper CSP will stop the browser from ever fetching or executing that script. Without CSP, the injected script runs with full access to the DOM, your users' cookies, and any tokens stored in memory.
Here is the simplest possible CSP header:
Content-Security-Policy: default-src 'self'
This tells the browser: only load resources from the same origin. Everything else is blocked. In practice, this breaks almost every real application immediately, which is why most developers abandon the effort.
The Problem: CSP Breaks Things
If CSP were easy, 78% of Laravel apps would not be skipping it.
Inline scripts are blocked. <script>alert('hello')</script> will not run unless you explicitly allow 'unsafe-inline'. But 'unsafe-inline' defeats most of the purpose of having a CSP at all.
Livewire injects inline scripts. Livewire 3 adds JavaScript directly to the page. Under a strict CSP, those scripts are blocked unless you use nonces.
Alpine.js uses eval. Alpine evaluates your x-data, x-on, and x-bind expressions using JavaScript's Function constructor, which is equivalent to eval. Browsers block this under CSP unless you allow 'unsafe-eval'.
Vite's dev server uses WebSockets and loads from localhost. In development, Vite HMR requires connections to localhost:5173. Your policy needs to account for that.
Third-party scripts have their own dependencies. Google Analytics loads from google-analytics.com, which loads from googletagmanager.com, which loads from doubleclick.net. You need to whitelist all of them or the tracking breaks silently.
Setting Up spatie/laravel-csp
The best way to add CSP to a Laravel app is with the spatie/laravel-csp package.
Installation
composer require spatie/laravel-csp
php artisan vendor:publish --provider="Spatie\Csp\CspServiceProvider" --tag="csp-config"
Registering the Middleware
In Laravel 11 and later, add the middleware to your bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Spatie\Csp\AddCspHeaders::class,
]);
})
Creating a Custom Policy Class
Create a policy class for your application:
php artisan make:csp AppPolicy
Here is a realistic starting policy for a Laravel app using Livewire, Alpine.js, a Google Font, and Google Analytics:
<?php
namespace App\Policies\Csp;
use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policies\Policy;
class AppPolicy extends Policy
{
public function configure(): void
{
$this
->addDirective(Directive::DEFAULT, Keyword::SELF)
->addDirective(Directive::SCRIPT, [
Keyword::SELF,
Keyword::NONCE,
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
])
->addDirective(Directive::STYLE, [
Keyword::SELF,
Keyword::NONCE,
'https://fonts.googleapis.com',
])
->addDirective(Directive::FONT, [
Keyword::SELF,
'https://fonts.gstatic.com',
])
->addDirective(Directive::IMG, [
Keyword::SELF,
'data:',
'https://www.google-analytics.com',
])
->addDirective(Directive::CONNECT, [
Keyword::SELF,
'https://www.google-analytics.com',
])
->addDirective(Directive::FRAME, Keyword::NONE)
->addDirective(Directive::OBJECT, Keyword::NONE)
->addDirective(Directive::BASE, Keyword::SELF)
->addDirective(Directive::FORM_ACTION, Keyword::SELF);
}
}
Register in config/csp.php:
'policy' => \App\Policies\Csp\AppPolicy::class,
Handling Nonces
A nonce (number used once) is a random string generated fresh for each HTTP request. You add it to your CSP header and to any inline <script> or <style> tag. The browser checks that they match.
Using Nonces in Blade
<script nonce="{{ csp_nonce() }}">
window.APP_URL = '{{ config('app.url') }}';
</script>
Livewire 3 Nonce Support
Livewire 3 has built-in nonce support:
@livewireScripts(['nonce' => csp_nonce()])
Livewire will attach the same nonce to any inline scripts it injects during page load and component updates.
Alpine.js and unsafe-eval
Alpine.js evaluates expressions at runtime using the Function constructor. Add 'unsafe-eval' to your script-src:
->addDirective(Directive::SCRIPT, [
Keyword::SELF,
Keyword::NONCE,
Keyword::UNSAFE_EVAL, // Required for Alpine.js
])
Note that 'unsafe-eval' is significantly less dangerous than 'unsafe-inline'. It allows dynamic code evaluation but does not permit arbitrary inline script injection.
CSP for Vite: Development vs Production
Create a dedicated development policy:
class DevPolicy extends AppPolicy
{
public function configure(): void
{
parent::configure();
$this->addDirective(Directive::CONNECT, [
Keyword::SELF,
'ws://localhost:5173',
'http://localhost:5173',
]);
$this->addDirective(Directive::SCRIPT, [
Keyword::SELF,
Keyword::NONCE,
Keyword::UNSAFE_EVAL,
'http://localhost:5173',
]);
$this->addDirective(Directive::STYLE, [
Keyword::SELF,
Keyword::NONCE,
Keyword::UNSAFE_INLINE,
'https://fonts.googleapis.com',
]);
}
}
Switch between policies in config/csp.php:
'policy' => app()->isProduction()
? \App\Policies\Csp\AppPolicy::class
: \App\Policies\Csp\DevPolicy::class,
Common Mistakes
Using unsafe-inline everywhere. This turns CSP into theatre. If you add 'unsafe-inline' to script-src, an attacker who can inject a script tag can still execute it freely. Use nonces instead.
Forgetting font sources. A common symptom: fonts are broken in staging but fine locally. Always include your font CDN in font-src.
Breaking third-party embeds. If you embed YouTube, Stripe, or Intercom, those widgets load resources from their own domains. Check each vendor's CSP requirements.
Not using report-only first. Deploying an enforcing CSP without testing is one of the fastest ways to break your application for all users simultaneously.
Report-Only Mode First
Always deploy as Content-Security-Policy-Report-Only first. This behaves identically in terms of violation detection but blocks nothing.
Deployment Sequence
- Deploy with
Content-Security-Policy-Report-Only - Collect violation reports for 1-2 weeks
- Update your policy to resolve legitimate violations
- Switch to enforcing
Content-Security-Policy - Keep a
report-uridirective for ongoing monitoring
Setting Up a Report Endpoint
// routes/web.php
Route::post('/csp-report', function (Request $request) {
logger()->warning('CSP violation', $request->json()->all());
return response()->noContent();
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
CSP and StackShield
StackShield checks your application's HTTP response headers on every scan, including whether a Content-Security-Policy header is present and whether it uses obviously weak directives like 'unsafe-inline' in script-src without a nonce fallback.
If your CSP is missing or misconfigured, StackShield will flag it and can alert you when something changes after a deploy.
Run a free StackShield scan to see what your app's current header posture looks like.
Summary
CSP takes an afternoon to implement properly. Most of that time is spent in report-only mode watching violations come in. The enforcing deploy, once you reach it, tends to be uneventful.
- Install
spatie/laravel-cspand register the middleware on yourwebroutes - Create a custom policy class with specific directives for your tech stack
- Use
Keyword::NONCEinstead of'unsafe-inline'for inline scripts and styles - Pass the nonce to
@livewireScriptsso Livewire's inline scripts are allowed - Create a separate dev policy that permits Vite's HMR server
- Deploy as
report_only_policyfirst and collect violations for at least a week - Monitor your headers with StackShield to catch regressions after deploys
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.
Frequently Asked Questions
What is Content Security Policy in Laravel?
A Content Security Policy (CSP) is an HTTP response header that instructs the browser on which sources are allowed to load scripts, styles, fonts, images, and other resources. In Laravel, you add a CSP header via middleware. The most common approach is using the spatie/laravel-csp package, which lets you define policies as PHP classes. Without a CSP, browsers will load resources from any origin, which makes cross-site scripting (XSS) attacks significantly more effective.
Does CSP break Livewire or Alpine.js?
It can, but only if your policy is too restrictive or incorrectly configured. Livewire 3 supports CSP nonces natively. If you pass a nonce to your Livewire scripts, Livewire will attach that nonce to any inline scripts it generates. Alpine.js evaluates expressions using a Function constructor, which browsers block under a strict CSP unless you allow unsafe-eval. Using report-only mode first will show you exactly which directives are being blocked before you enforce the policy.
Should I use CSP report-only mode first?
Yes, always. The Content-Security-Policy-Report-Only header sends violation reports to a reporting endpoint without actually blocking anything. This lets you discover what your policy would break before it breaks it in production. Deploy in report-only mode, collect violations for a week or two, adjust the policy until violations stop, then switch to the enforcing Content-Security-Policy header.
What is the difference between CSP nonces and hashes?
Both nonces and hashes are alternatives to unsafe-inline that let specific inline scripts run. A nonce is a random value generated per request, added to the CSP header and to any inline script tags via a nonce attribute. A hash is a SHA-256 digest of the exact script content. Nonces are easier to use with dynamic content like Livewire. Hashes are better for static inline scripts that never change.
How do I add CSP headers to a Laravel API?
For a pure JSON API, you technically do not need a CSP header because there is no browser rendering HTML. However, if your API has any web-accessible documentation UI or admin interface, those routes should be covered by a CSP. Apply the CSP middleware only to your web routes group and exclude your API routes group.
Related Articles
Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
SecurityAutomated Security Testing in Laravel CI/CD Pipelines
How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.
SecurityLaravel Is Now on the Same Federal Vulnerability List as Apple. Here Is What That Means.
CISA added the Livewire RCE vulnerability (CVE-2025-54068) to the Known Exploited Vulnerabilities catalog, linking it to active exploitation by Iranian APT MuddyWater. A Laravel ecosystem package is now on the same US government list as Apple and Microsoft. Here is what that changes for your team.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.