Security 12 min read

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.

Matt King
Matt King
May 30, 2026
Last updated: May 30, 2026
Laravel Content Security Policy: Configure CSP Without Breaking Your App

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

  1. Deploy with Content-Security-Policy-Report-Only
  2. Collect violation reports for 1-2 weeks
  3. Update your policy to resolve legitimate violations
  4. Switch to enforcing Content-Security-Policy
  5. Keep a report-uri directive 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-csp and register the middleware on your web routes
  • Create a custom policy class with specific directives for your tech stack
  • Use Keyword::NONCE instead of 'unsafe-inline' for inline scripts and styles
  • Pass the nonce to @livewireScripts so Livewire's inline scripts are allowed
  • Create a separate dev policy that permits Vite's HMR server
  • Deploy as report_only_policy first and collect violations for at least a week
  • Monitor your headers with StackShield to catch regressions after deploys
Free security check

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.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 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.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.