Generating an Effective Content Security Policy with your Laravel React App

tom Tom, 27th February 2024

I recently had an interesting problem to solve. I'd built a brand new author website on a shiny installation of Laravel 10, utilising its out-of-the-box frontend start kit for React. Laravel Breeze offers React scaffolding via an Inertia frontend implementation. As the website explains "Inertia allows you to build modern, single-page React and Vue applications using classic server-side routing and controllers."

This is all brilliant. I'm a big fan of traditional backend PHP coding and frameworks and this approach lets you build a modern React app on a Laravel backend communicating to whatever endpoints you need, via a neat API. It's seemingly the perfect solution for modern web development (assuming you're not too bothered about SEO - but that's for another post.) You can get up and running very quickly with a pretty cool setup. However the issue I ran into was implementing an effective Content Security Policy.

Content Security Policies are a bit of a pain in the codebase. Implementing them on existing sites can be extremely fiddly, especially sites where you've added various third party scripts over time, or used libraries or hotlinked out to external scripts. I guess that's kind of the point of CSPs though - to make sure the code that's running on the site has explicit permission to do so.

As this was a brand new site I didn't have these legacy issues to contend with. I did, however, have to contend with a rather complicated set of technologies upon which the new site had been built. That's one of my biggest bugbears with modern web development. The stack of technologies upon which we're all developing requires such a wide breadth and depth of knowledge that sometimes it can feel like we've engineered ourselves into a hole. Doing what would previously had been a relatively simple task can be made more difficult by having to navigate and coerce these technologies to work together.

In this situation I found that when I was compiling my app using 'npm run build' it was adding some Javascript inline at the bottom of the app. This was then causing my Content Security Policy to be invalid.

I had 3 options.

  1. Either allow 'unsafe-inline' Javascript - This clearly isn't a good idea. The whole point of a content security policy is to block potentially bad code from running on your site. If someone managed to inject code into the webpage of a site that allowed any random inline Javascript to run, it isn't protected at all.
  2. Somehow configure the site to compile this JS as a separate JS file, rather than inline it in <script> tags in the source of my HTML.
  3. Use the 'nonce' approach and add a unique string to each script tag and pass the coresponding nonce value in the content security policy header.

Initially I opted for option 1 because that's by far the easiest. However after doing this my security rating was still pretty poor, and as I've stated above, a policy that allows unsafe-inline JS doesn't really protect a site much at all. To be honest I wasn't too worried about anyone being able to inject code into this site because it's pretty simple. But for the sake of improving my score I thought I'd see if I could find a better solution.

Next I tried to convince the site to compile as separate JS files. I thought this would be possible and I've found references to this elsewhere online. It was suggested that adding 'INLINE_RUNTIME_CHUNK=false' to my .env file would be all it took. However I found this just didn't work for me. Perhaps because I was building a Laravel React app using Inertia.js. Or perhaps it was because I was using Vite rather than Webpack. I'm unsure. Either way, after trying various options around this idea I gave up.

That left me with option 3. I first tried using Laravel-csp from Spatie. I'm a big fan of Spatie code and have used their libraries many times before, but I just couldn't get on with this one. I was able to get it working ok, and could configure my CSP. (Although configuring my CSP can also be done in a one-liner in my .htaccess file). My issue was applying the nonce to the scripts at compile time. Again, ages was spent trying to make this work in vain.

Then I stumbled upon this post on Stack Overflow.

Which was the answer I'd been looking for. I'll repeat the steps below, just in case the post disappears. I hate it when I find a solution online and someone links to what sounds like the thing you've been searching for, only to find it goes to a 404!

Step 1: Create a new middleware:

Here's my new Laravel Middleware for this:


<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Closure;
use Log;

class SecurityHeadersMiddleware
{
    /**
     * @throws Exception
     */
    public function handle(Request $request, Closure $next)
    {
        $nonce = base64_encode(random_bytes(22));

        $request->attributes->add(['csp_nonce' => $nonce]);

        $response = $next($request);

        if (!app()->environment('local')) {
            $response->headers->set('Content-Security-Policy', "script-src 'self' 'nonce-{$nonce}'; object-src 'none'; style-src 'self' fonts.googleapis.com");
        }

        return $response;
    }
}

Step 2: Add the middleware to your kernel.php


/**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\HandleInertiaRequests::class,
            \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
            //\Spatie\Csp\AddCspHeaders::class,
            \App\Http\Middleware\SecurityHeadersMiddleware::class,
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

Step 3: Create a new provider: app/Providers/NonceServiceProvider.php


<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class NonceServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot(): void
    {
        view()->composer('*', function ($view) {
            $view->with('csp_nonce', request()->attributes->get('csp_nonce'));
        });
    }
}

Step 4: Add the new provider to your config/app.php

Step 5: Add the nonce to your @route in resources/views/app.blade.php


<!-- Scripts -->
@routes(false, $csp_nonce)

Then, like magic, when you compile you should find the same nonce is sent in your new CSP and also appended to the script tag output by the call to @routes().

Genius and I'm very grateful for the help, so I hope this post helps someone else out there too.

More from our blog

18a win Netty 2024 award for Activibees.com

18a win Netty 2024 award for Activibees.com

29.02.24

We are delighted to announce that 18a has been recognised for its outstanding work in the "Web Design Agency of the Year - UK" category at… Read →

If your WordPress website looks broken, it could be because of this.

If your WordPress website looks broken, it could be because of this.

15.02.24

WordPress is the incredibly popular blogging-come-full-website platform that powers over 835 million websites* in 2024. It's functionality is extended by plugins, and one such very… Read →

WordPress Popup Builder plugin causing sites to get hacked

WordPress Popup Builder plugin causing sites to get hacked

31.01.24

A marketing agency recently asked us to help with a site they look after for a client because it was displaying odd behaviour - when… Read →