Implementing Content Security Policy (CSP) in Laravel

· 3 min read

Cross-Site Scripting (XSS) remains one of the most common web vulnerabilities. While Laravel's Blade engine escapes output by default, relying solely on escaping is not enough. A Content Security Policy (CSP) adds a crucial layer of defense by controlling which resources (scripts, styles, images) the browser is allowed to load.

What is CSP?

CSP is an HTTP header that tells the browser: "Only load scripts from these domains. Only load styles from these domains. Block everything else."

Example header:

Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.google.com;

If an attacker manages to inject <script src="http://evil.com/hack.js"></script>, the browser will refuse to load it because evil.com is not in the whitelist.

Implementing in Laravel

The easiest way involves using the spatie/laravel-csp package.

1. Installation

composer require spatie/laravel-csp

Publish the config:

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

2. Creating a Policy

Create a dedicated class to define your rules.

// app/Support/Csp/BasicPolicy.php

namespace App\Support\Csp;

use Spatie\Csp\Directive;
use Spatie\Csp\Policies\Policy;

class BasicPolicy extends Policy
{
    public function configure()
    {
        $this
            ->addDirective(Directive::BASE, 'self')
            ->addDirective(Directive::CONNECT, 'self')
            ->addDirective(Directive::DEFAULT, 'self')
            ->addDirective(Directive::FORM_ACTION, 'self')
            ->addDirective(Directive::IMG, 'self')
            ->addDirective(Directive::MEDIA, 'self')
            ->addDirective(Directive::OBJECT, 'none')
            ->addDirective(Directive::SCRIPT, 'self')
            ->addDirective(Directive::STYLE, 'self')
            // Allow Google Fonts
            ->addDirective(Directive::STYLE, 'https://fonts.googleapis.com')
            ->addDirective(Directive::FONT, 'https://fonts.gstatic.com');
    }
}

3. Registering the Policy

In config/csp.php, set the policy class:

'policy' => App\Support\Csp\BasicPolicy::class,

And add the middleware to bootstrap/app.php (or kernel.php in older Laravel):

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

Handling Inline Scripts (Nonces)

If you use tools like Vite or AlpineJS, you might have inline scripts. CSP blocks inline scripts (<script>...</script>) by default.

To fix this, we use a Nonce (number used once).

  1. Add addNonceForDirective(Directive::SCRIPT) to your policy.
  2. Use the nonce() helper in Blade.
{{-- In your layout --}}
<script nonce="{{ csp_nonce() }}">
    console.log('This script is safe because it has a nonce.');
</script>

Vite Integration

Laravel Vite plugin creates script tags automatically. To add nonces to them, update your layout.blade.php:

@vite(['resources/css/app.css', 'resources/js/app.js'], buildDirectory: 'build', nonce: csp_nonce())

Troubleshooting

CSP can break your site if configured incorrectly (e.g., blocking Google Analytics or Stripe).

Tip: Start in "Report Only" mode. In config/csp.php, set 'report_only_mode' => true. The browser will log violations to the console (or a configured endpoint) without actually blocking the content. Once you see no violations, switch to enforcing mode.

Conclusion

CSP is not a "set and forget" feature; it requires maintenance as you add new 3rd party services. However, it is the single most effective mitigation against XSS. For any serious Laravel application, a strict CSP is mandatory.

Comments