# 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.

**Author:** Matt King | **Published:** May 30, 2026 | **Category:** Security

---

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

```bash
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`:

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \Spatie\Csp\AddCspHeaders::class,
    ]);
})
```

---

## Creating a Custom Policy Class

Create a policy class for your application:

```bash
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
<?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`:

```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

```blade
<script nonce="{{ csp_nonce() }}">
    window.APP_URL = '{{ config('app.url') }}';
</script>
```

### Livewire 3 Nonce Support

Livewire 3 has built-in nonce support:

```blade
@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:

```php
->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:

```php
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`:

```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

```php
// 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](/free-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

---

## 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.

