Open-source project
spatie/laravel-csp avatar
spatie/laravel-csp

laravel-csp turns a CSP header into a config file

Set content security policy headers in a Laravel app

870 stars102 forksPHPMIT

At a glance

What is it?
spatie/laravel-csp adds Content Security Policy headers to a Laravel application through presets, global middleware and per-request nonce generation, with a report-only mode for testing a policy before enforcing it. It is a convenience layer over a header most teams still need to learn, and the package says so itself.
Who is it for?
laravel-csp fits teams already running Laravel 12 or 13 on PHP 8.3 who want a structured CSP policy with report-only rollout and per-route preset overrides rather than a hand-written header string. It does not fit a project that cannot move to PHP 8.3, and it will not teach you the policy itself, since the documentation explicitly defers to Mozilla's CSP reference.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The header that decides which sites your pages may contact

By default, all scripts on a webpage are allowed to send and fetch data to any site they want, and the package description treats that as a security problem rather than a default to shrug at. The scenario the documentation sketches is a JavaScript dependency that ships every keystroke, including passwords, to a third party site. The difficulty is detection: this behaviour is easy to hide, so it is nearly impossible to spot unless somebody manually reads all the JavaScript on the site. Content Security Policy headers answer it by dictating which sites your site is allowed to contact. The target user is a Laravel developer who wants that restriction expressed as configuration rather than as a header string typed into middleware, and who needs a way to try a policy without breaking the application on day one.

Presets decide which directives end up in the header

The mechanism is a preset class system. In the published configuration file, the presets key holds a list of class names, and the comment above it states that a valid CSP preset is any class that implements Spatie\Csp\Preset. The default configuration registers a single one, Spatie\Csp\Presets\Basic. Presets are therefore the unit of policy: each preset knows which directives and keywords belong in the header, and the middleware assembles the final header from the presets that apply to the request. Two separate preset lists exist, presets for the enforced policy and report_only_presets for the reporting variant, and the same split is mirrored for individual directives through the directives and report_only_directives keys. That separation is what lets a team measure a policy before enforcing it.

Installing the package and publishing config/csp.php

Requirements are stated up front: this package requires PHP 8.3 or higher and Laravel 12 or 13. Installation is a single Composer command:

bash
composer require spatie/laravel-csp

The config file is not published automatically. You publish it with the csp-config tag:

bash
php artisan vendor:publish --tag=csp-config

That writes the file to config/csp.php, and the published content starts with the preset and directive keys:

php
    'presets' => [
        Spatie\Csp\Presets\Basic::class,
    ],

    /**
     * Register additional global CSP directives here.
     */
    'directives' => [
        // [Directive::SCRIPT, [Keyword::UNSAFE_EVAL, Keyword::UNSAFE_INLINE]],
    ],

The commented Directive::SCRIPT and Keyword::UNSAFE_EVAL examples show the shape of the directive syntax, so extra rules are added by uncommenting and editing rather than by inventing a new config format. Once published, config/csp.php is an ordinary Laravel config file and follows the usual environment variable pattern for anything you want to vary per environment.

Global middleware, route middleware and the preset override

The headers are attached through middleware, and there are three ways to use it. The broadest is global registration in bootstrap/app.php:

php
use Spatie\Csp\AddCspHeaders;

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

That applies the headers to all responses of the app. The narrower option is route level, which is useful when one page needs a different policy:

php
// In your routes file
Route::get('my-page', 'MyController')
    ->middleware(AddCspHeaders::class);

The third form passes a preset class as a middleware parameter:

php
// In your routes file
Route::get('my-page', 'MyController')
    ->middleware(AddCspHeaders::class . ':' . MyPreset::class);

The documentation is explicit about the semantics of that last form: the given preset overrides the ones configured in config/csp.php for that specific route or group of routes. So the global config is the baseline and the route parameter is an exception, not an addition, which is worth keeping in mind when a page behaves oddly.

Report-only mode and the reporting endpoint settings

Rolling out a policy without breaking anything is the part of CSP that punishes teams who skip it, and the configuration has explicit support for it. The report_only_presets and report_only_directives keys hold the policy that is sent as report-only instead of enforced, and the comments describe this as a way to test a new policy or changes to an existing one without breaking anything. Where violations go is configured separately:

php
    'report_uri' => env('CSP_REPORT_URI', ''),

    /*
     * Optional separate report url for the report-only policy. When empty,
     * the report-only policy falls back to `report_uri` above.
     */
    'report_only_uri' => env('CSP_REPORT_ONLY_URI', ''),

There is a second layer on top of that, the reporting endpoint names. The report_to and report_only_to keys hold a name, and the reporting_endpoints array maps names to URLs that are sent in the Reporting-Endpoints HTTP header. Each of these falls back to the value above it when left empty, so a single endpoint is a one line configuration and a split setup is only needed when you want the enforced and report-only policies to report to different collectors. The configuration also names https://report-uri.com/ as a service that could be used for the report uri.

Nonces, the Vite escape hatch and the enabled switch

Inline scripts and styles are the hard part of any CSP, and the package handles them with a nonce generator. The relevant settings look like this:

php
    'enabled' => env('CSP_ENABLED', true),

    /*
     * Headers will be added when Vite is hot reloading.
     */
    'enabled_while_hot_reloading' => env('CSP_ENABLED_WHILE_HOT_RELOADING', false),

    /*
     * The class responsible for generating the nonces used in inline tags and headers.
     */
    'nonce_generator' => Spatie\Csp\Nonce\RandomString::class,

Three separate controls hide in there. The enabled switch turns header generation off entirely, which is the kill switch you want when debugging. The nonce generator is a swappable class, and the default is RandomString. The nonce_enabled setting, documented alongside the generator, lets you disable automatic nonce generation and handling, and the documentation is honest that this is useful when you want unsafe-inline for scripts and styles and cannot add inline nonces, and that the policy will be less secure as a result. The Vite hot reloading flag exists because local development with a dev server behaves differently from production, and enabling headers there tends to break the asset pipeline.

What the package deliberately does not explain

The documentation states its own limits, and it is unusual for a package README to be this clear. It says the readme does not aim to fully explain all the possible usages of CSP and its directives, and it points readers to Mozilla's documentation on the Content Security Policy, with an in-depth newsletter issue on the subject as a second resource. Read that as a warning about expectations: the package handles the mechanics of emitting headers, not the judgement about which sources to trust. A developer can ship a syntactically valid policy that still permits everything, and nothing in this package will point that out. The alternative to using it is writing the header yourself in custom middleware, which gives full control and no dependency but means maintaining the directive syntax, the per-route overrides, the nonce plumbing and the reporting configuration by hand. That trade is worth it only for an application with unusual requirements, since the package covers the same ground with a config file and a middleware class.

MIT licence, recent releases and the upgrade path

The project is MIT licensed, which is the permissive end of the scale and easy to adopt inside a commercial application without special handling. Maintenance is current: the last push to the repository was on 2026-10-19, the repository is not archived, and the release list shows 3.28.3 on 2026-09-03, 3.29.0 on 2026-09-09 and 3.30.0 on 2026-10-19, so patch and minor releases are landing regularly. The version numbers also suggest a package that is kept compatible with Laravel's own release cadence, which is relevant because the requirement of Laravel 12 or 13 means a future Laravel release can end support. The repository layout helps with upgrades: there is an UPGRADE.md next to CHANGELOG.md, a src/ and tests/ tree, a config/ directory, and a phpunit.xml.dist for the test setup. The upgrade cost of this package is therefore mostly reading UPGRADE.md and the changelog between your pinned version and the target, not rewriting your middleware wiring.

Editorial conclusion

laravel-csp fits teams already running Laravel 12 or 13 on PHP 8.3 who want a structured CSP policy with report-only rollout and per-route preset overrides rather than a hand-written header string. It does not fit a project that cannot move to PHP 8.3, and it will not teach you the policy itself, since the documentation explicitly defers to Mozilla's CSP reference. Before switching a production site over, run the policy in report-only mode with CSP_REPORT_URI set, confirm the violations your pages actually trigger, and only then rely on the enforced presets defined in config/csp.php.

Frequently asked questions

How do I install spatie/laravel-csp?

Run composer require spatie/laravel-csp, then publish the configuration file with php artisan vendor:publish --tag=csp-config, which writes it to config/csp.php. The package requires PHP 8.3 or higher and Laravel 12 or 13.

How do I add CSP headers to every response in a Laravel app?

Register Spatie\Csp\AddCspHeaders::class as global middleware in bootstrap/app.php using the withMiddleware closure and the append method. You can also apply the middleware to individual routes or route groups.

Can I test a CSP policy before enforcing it?

Yes. The report_only_presets and report_only_directives settings hold a policy that is sent as report-only rather than enforced, which the documentation describes as useful for testing a new policy or changes without breaking anything. Violations can be sent to the report uri.

How does laravel-csp handle inline scripts and styles?

It generates nonces through a configurable nonce generator class, with Spatie\Csp\Nonce\RandomString as the default. Automatic nonce generation can be switched off with the nonce_enabled setting, though the documentation notes that this makes the policy less secure.

Can a single route use a different CSP preset?

Yes. You can pass a preset class as a parameter to the middleware, for example AddCspHeaders::class . ':' . MyPreset::class, and that preset overrides the ones configured in config/csp.php for that route or group of routes.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/laravel-csp on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spatie-laravel-csp.svg)](https://hysenlabs.com/projects/spatie-laravel-csp)