Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Csp Laravel Package

spatie/laravel-csp

Spatie's Laravel CSP package makes it easy to set Content Security Policy headers in Laravel apps with configurable policies and directives. It helps restrict which sites scripts can contact, reducing risks from malicious or compromised JavaScript dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the config:

composer require spatie/laravel-csp
php artisan vendor:publish --tag=csp-config

Register the middleware globally in bootstrap/app.php (Laravel 11+):

use Spatie\Csp\AddCspHeaders;

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(AddCspHeaders::class);
})

On Laravel 10 and below, add Spatie\Csp\AddCspHeaders::class to $middleware (or the web group) in app/Http/Kernel.php.

Then verify headers are actually being emitted before touching any configuration:

curl -sI https://your-app.test | grep -i content-security-policy

The default config/csp.php ships with Spatie\Csp\Presets\Basic enabled, so you should immediately see a Content-Security-Policy header. Open DevTools → Network → your document → Response Headers to confirm, and keep the Console tab open — every blocked resource appears there with the exact directive that rejected it.

Where to look first:

  1. config/csp.php — presets, report_only_presets, directives, report_only_directives, enabled.
  2. vendor/spatie/laravel-csp/src/Presets/ — read the actual Basic preset and any vendor preset you enable. Each is ~10 lines of $policy->add(...) and tells you exactly which hosts are being whitelisted. Don't enable a preset blind.
  3. vendor/spatie/laravel-csp/src/Directive.php and Keyword.php — the enums you'll use in your own preset.

The right first use case is not shipping a strict policy. It's this: enable Basic, add report-only presets for whatever third parties you actually use (Stripe, Google Fonts, Sentry, PostHog…), point report_uri at something that collects reports, and let it run for a week. That gives you a real inventory of what your app loads instead of guessing.

Implementation Patterns

Baseline globally, overrides per route group. Keep the app-wide policy in config/csp.php and override it where the surface differs — a marketing page with an embedded Vimeo player, an admin panel, a widget route intended to be iframed:

Route::get('embed/{post}', [EmbedController::class, 'show'])
    ->middleware(AddCspHeaders::class.':'.App\Http\Csp\EmbeddablePreset::class);

The preset passed as a middleware parameter replaces the configured presets for that route, which is exactly what you want for a frame-ancestors route that must not inherit the app's 'self'-only default.

Write one app preset instead of scattering config directives. Presets are plain classes, easy to review in a PR and easy to test:

namespace App\Csp;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policy;
use Spatie\Csp\Preset;

class AppPreset implements Preset
{
    public function configure(Policy $policy): void
    {
        $policy
            ->add(Directive::DEFAULT, Keyword::SELF)
            ->add(Directive::SCRIPT, Keyword::SELF, 'https://cdn.example.com')
            ->add(Directive::STYLE, Keyword::SELF)
            ->add(Directive::IMG, Keyword::SELF, 'data:')
            ->add(Directive::FONT, Keyword::SELF, 'https://fonts.bunny.net')
            ->add(Directive::CONNECT, Keyword::SELF, 'https://api.example.com')
            ->addNonce(Directive::SCRIPT)
            ->addNonce(Directive::STYLE);
    }
}

Register it in presets and delete Basic — if you keep both they are merged, and the union is usually looser than either.

Use nonces for inline scripts, not 'unsafe-inline'. With nonces enabled (the default), the package generates one nonce per request and the Blade directive emits the attribute:

<script @cspNonce>
    window.__BOOTSTRAP__ = @json($bootstrap);
</script>

csp_nonce() gives you the same value for places Blade directives don't reach (a raw PHP view, an attribute you build manually, an inline event handler you're forced to keep). Any script tag that loads your own JavaScript through a bundler is fine under 'self'; nonces are for the handful of inline boots, GTM snippets, and analytics initialisers.

Staged promotion with report-only. The report_only_presets / report_only_directives arrays produce a separate Content-Security-Policy-Report-Only header that blocks nothing but reports everything. The workflow:

  1. Put a stricter candidate policy in report_only_presets, keep the current one in presets.
  2. Set CSP_REPORT_URI (or the newer CSP_REPORT_TO + reporting_endpoints) to a collector — report-uri.com, a Sentry CSP endpoint, or a tiny controller route.
  3. Diff the violations against your actual asset list over a few days.
  4. Promote the candidate to presets and empty report_only_presets.

Note report_only_uri / report_only_to fall back to the main values when empty — set them only if you want separate sinks.

Turn it off per environment. CSP_ENABLED=false in .env, plus enabled_while_hot_reloading for local Vite. For a shared staging environment that mirrors production, keep it on so violations surface before release.

Assert the header in tests. CSP is security-relevant behaviour and regressions happen when someone adds a CDN to a layout:

it('sends a csp header', function () {
    $this->get('/')->assertHeader('Content-Security-Policy');
});

it('allows the embed route to be framed', function () {
    $this->get('/embed/1')
        ->assertHeader('Content-Security-Policy')
        ->assertSee('frame-ancestors', false);
});

For anything non-trivial, snapshot the header string — an unexpected directive appearing in the diff is the point.

Meta tag when headers aren't available. @cspMetaTag in your layout <head> emits <meta http-equiv="Content-Security-Policy" ...> for pages served from static HTML, a CDN that strips headers, or an environment where middleware can't run.

Gotchas and Tips

  • Local dev looks broken. enabled_while_hot_reloading defaults to false, so the middleware silently skips when Vite is hot. If you're debugging "the header isn't there", check public/hot before you check your config.
  • Nonces and cached HTML are incompatible. A nonce is per-request; a full-page cache or CDN-cached document serves a stale nonce and every inline script breaks. Either exclude those pages from caching, or supply a per-response nonce via a custom nonce_generator, or fall back to 'unsafe-inline' with nonces disabled (CSP_NONCE_ENABLED=false) — explicitly accepting the weaker policy.
  • 'unsafe-inline' is ignored when a nonce or hash is present. Adding both doesn't loosen the policy; modern browsers drop 'unsafe-inline'. This bites people adding 'unsafe-inline' to "quickly fix" a breakage while nonces are still on.
  • Presets are additive, never subtractive. Two presets that both touch script-src merge, and you cannot remove a directive another preset set. If a vendor preset is too broad, copy the lines you need into your own preset instead of stacking.
  • Combining 'none' with anything else produces a directive browsers may reject or ignore — 'none' must be alone. Watch for this if you add a global directives entry to a policy that already defines that directive in a preset.
  • Middleware is global means all responses. JSON APIs, file downloads and streamed responses all get the header. Harmless but noisy; if you serve an API from the same app, prefer the web group or per-route middleware rather than append on the global stack.
  • Meta tags can't do everything. frame-ancestors, report-uri, and sandbox are ignored in <meta> CSP. If you need framing protection, it must be a header.
  • report-uri is legacy. Newer browsers prefer the Reporting API via report_to + reporting_endpoints. Configure both during transition or you'll get reports from some browsers and silence from others.
  • Vite, Livewire, Alpine. Alpine's expression compiler uses new Function in the default build, so a strict script-src without 'unsafe-eval' will break Livewire/Alpine interactivity. Use Alpine's CSP build, or accept Keyword::UNSAFE_EVAL on script-src and document why.
  • Inline event handlers (onclick="...") are not covered by nonces. They need 'unsafe-hashes' plus a hash, or removal. Treat finding them as a refactor ticket rather than a config change.
  • Debugging order. Browser console first (it names the blocked directive and URL), then the response header, then the preset source. If the header is present but the page is broken, you're missing a host; if the header is absent, it's CSP_ENABLED, hot reload, or middleware registration.
  • Extension points worth knowing: nonce_generator (swap RandomString for something edge-cache-friendly), a custom Preset class, and route-level preset injection. Those three cover almost every real-world requirement without forking the package.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections