CSP in Laravel with Vite

In this article, I will show you how we can configure Content Security Policy headers with Laravel and Vite.

· 6 min read

This article assumes that you have some level of experience working with Laravel and Vite.

What is CSP?

CSP or Content Security Policy, is a security feature used in web browsers to protect against cross-site scripting (XSS) and other types of attacks. It is a policy that a website owner can specify in the HTTP header of their web page to instruct the user's browser on which sources of content are allowed to be loaded by the page.

CSP works by allowing website owners to specify a whitelist of trusted sources of content, such as scripts, stylesheets, images, and other resources, and preventing any content from being loaded from sources that are not on the whitelist. This helps to prevent attackers from injecting malicious code into a web page or stealing sensitive user data by forcing the browser to only load content from trusted sources.

Where should I configure CSP?

CSP headers can be configured within the program using code, or in the server. Configuring it in the server may lead to some limitations such as inability to dynamically apply nonce or hashes, need to change server configuration every time when content sources change etc. Therefore, it is recommended that we do it at the code level for more flexibility.

Using the code, we can apply CSP in two ways, one is by using meta tags inside the head element of our HTML document and another by adding headers in our server responses.

CSP in Laravel

Here in this article, I will be showing how we can configure CSP headers in standalone Laravel installation as well as Laravel with some frontend frameworks bundled with Vite. As Laravel provides an easy and flexible response object, we will be using it set our CSP headers using a package.

We will be using a Laravel package spatie/laravel-csp in order to configure CSP in our Laravel app. You can follow the installation instructions on the GitHub page. Install the package and publish the provided CSP configuration. Also as shown in the GitHub page, register Spatie\Csp\AddCspHeaders::class in your web or api (if required) middleware groups.

This package configures CSP headers using Policy files. The package provides a pre-configured Basic policy out of the box. It might be sufficient for many, but I will be creating a custom Policy file to add my custom sources.

Create a folder named Support inside your app directory. Inside Support directory, add a file named CSP.php. We will add our custom policies in this CSP.php file.

<?php

namespace App\Support;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policies\Basic;
use Spatie\Csp\Value;
use Spatie\Csp\Scheme;

class CSP extends Basic
{
    public function configure()
    {
        parent::configure();

        $this
        ->addDirective(Directive::IMG, [
            'https://images.google.com',
        ]);
    }
}

In my custom config file above, I used Basic policy provided by the package as my starting point and added a new directive for img-src allowing to serve images from https://images.google.com in my app. With this configuration added, response CSP headers from my app will contain headers configured inside Basic policy and my custom policy. The configurations in the Basic policy provided by the package look like this:

<?php

namespace Spatie\Csp\Policies;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;

class Basic extends Policy
{
    public function configure()
    {
        $this
            ->addDirective(Directive::BASE, Keyword::SELF)
            ->addDirective(Directive::CONNECT, Keyword::SELF)
            ->addDirective(Directive::DEFAULT, Keyword::SELF)
            ->addDirective(Directive::FORM_ACTION, Keyword::SELF)
            ->addDirective(Directive::IMG, Keyword::SELF)
            ->addDirective(Directive::MEDIA, Keyword::SELF)
            ->addDirective(Directive::OBJECT, Keyword::NONE)
            ->addDirective(Directive::SCRIPT, Keyword::SELF)
            ->addDirective(Directive::STYLE, Keyword::SELF)
            ->addNonceForDirective(Directive::SCRIPT)
            ->addNonceForDirective(Directive::STYLE);
    }
}

To learn more about directives, their values and their purposes, you can refer to MDN Web Docs.

In the last two lines of the Basic policy, we can see addNonceForDirective applied for script and style directives. This allows our app to generate a nonce value and apply to our inline CSS and JS.

Nonce and Hashes

Nonce (number used once) is a randomly generated string that is added to the HTTP response header. The same nonce value is added to the inline JavaScript and CSS scripts to allow their inline execution as by default CSP doesn't allow inline execution of scripts. The nonce value is unique for each request and response.

Hashes in CSP have similar use case as nonce but hashes are cryptographic hash values calculated using the inline CSS and JS content in the application. The hash value for a codebase is fixed until the underlying CSS or JS change.

We have created a custom policy file but haven't applied it yet. To make our custom policy file active we need to change the csp.php file configs inside our config directory.

<?php

return [

    /*
     * A policy will determine which CSP headers will be set. A valid CSP policy is
     * any class that extends `Spatie\Csp\Policies\Policy`
     */
    'policy' => App\Support\CSP::class,

    /*
     * This policy which will be put in report only mode. This is great for testing out
     * a new policy or changes to existing csp policy without breaking anything.
     */
    'report_only_policy' => '',

    /*
     * All violations against the policy will be reported to this url.
     * A great service you could use for this is https://report-uri.com/
     *
     * You can override this setting by calling `reportTo` on your policy.
     */
    'report_uri' => env('CSP_REPORT_URI', ''),

    /*
     * Headers will only be added if this setting is set to true.
     */
    'enabled' => env('CSP_ENABLED', true),

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

In this config, change the value of policy key to App\Support\CSP::class and save.

If you have used any inline styles or scripts in your Laravel blade files, you need to add a nonce value in those tags to allow their inline execution. For e.g:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style nonce="{{csp_nonce()}}">
        body {
            background: #fff;
        }
    </style>
    <script nonce="{{csp_nonce()}}">
        console.log('Hello World');
    </script>
</head>
<body>
    <h1>This is simple Laravel Blade page.</h1>
</body>
</html>

Now, with this setup, you should be able to see configured CSP headers added to the response for each request.

Frontend Integration

Until now we have added CSP headers for our standalone Laravel installation. If you have any frontend frameworks such as React or Vue installed alongside Laravel, we need to configure a few things to make it all work. I assume you are using Vite as your build tool.

As mentioned above, to execute styles and scripts inline, we need to add a nonce value. While integrating React or Vue in our Laravel app, they also inject JS scripts inside our blade file where we must add nonce to allow them to execute.

To do that, we need to add a meta property with our nonce value inside the head of our blade file and also configure a nonce generator with Vite in our csp.php config file.

You can add CSP meta property as below in your welcome.blade.php file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta property="csp-nonce" content="{{csp_nonce()}}">
    <style nonce="{{csp_nonce()}}">
        body {
            background: #fff;
        }
    </style>
    <script nonce="{{csp_nonce()}}">
        console.log('Hello World');
    </script>
    {{-- React Frontend Scripts --}}
    @viteReactRefresh
    @vite('resources/js/app.jsx')
</head>
<body>
    <h1>This is simple Laravel Blade page.</h1>
</body>
</html>

Now that we have added our meta property, let us configure our nonce generator. For generating nonce, Vite provides a default method useCspNonce() out of the box. Create a file named ViteNonceGenerator.php inside app\Support directory and add below code:

<?php

namespace App\Support;

use Illuminate\Support\Facades\Vite;
use Spatie\Csp\Nonce\NonceGenerator;

class ViteNonceGenerator implements NonceGenerator
{
    public function generate(): string
    {
        return Vite::useCspNonce();
    }
}

Now add this file as our nonce generator in config/csp.php file.

...
/*
 * The class responsible for generating the nonces used in inline tags and headers.
*/
'nonce_generator' => App\Support\ViteNonceGenerator::class,
...

With all this done, you should see CSP headers in your application without any issues. If you inspect your HTML DOM in your browser, you should be able to see a nonce value added to all styles and scripts. If you do not see the nonce value, no worries, most of the modern browsers hide the value and just show the property name, that implies you have successfully configured a correct nonce value in your header and in your scripts.

Here is a sample CSP header value generated with above configs:

base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://images.google.com;media-src 'self';object-src 'none';script-src 'self' 'nonce-EIY6JvzINHbS6VfUoJoET6L0Bldk2llgAUjQ9VdK';style-src 'self' 'nonce-EIY6JvzINHbS6VfUoJoET6L0Bldk2llgAUjQ9VdK'

Thank you for reading.

Please let me know in the comments whether you were able to add CSP successfully to your Laravel application or not.