Photo of Christoph Rumpel

Laravel Response Caching and CSP

Caching is lovely, and the Content Security Policy is incredible. But when you put them together... 🤯 Let me show you the problems I encountered, and how I fixed them.

The Setup

While redesigning my blog I installed Spatie's Laravel ResponseCache package. It caches the entire response and returns it for same requests. This makes the site super fast because the request doesn't need to go through the whole application again.

This week I added a CSP (Content Security Policy) header to my blog. It adds another layer of security to it because it lets you define what resources are allowed to be loaded. In my last article I explained this topic in detail, so check it out if you want to know more about it.

The Problem

A day after publishing my CSP article, I noticed that the CSP nonces are not changing on my production site. You can set nonces for CSP directives in the header and use them in your markup to define which inline scripts or styles are valid. It works like this.

<script nonce="1234"></script>

But as mentioned in the CSP article, this only makes sense, when the nonce changes with every request. It is the same with CSRF tokens. But on my production site, they stayed the same!

Different to the local site, caching is turned on on my production site. That's why I didn't recognize it first. I had to turn caching off there as well immediately; security > performance.

The Solution

Since CSP is quite a new topic, I didn't find many resources on this problem. But it was enough to realize this is a common problem with no general solution. So I opened an issued, to ask Spatie about that problem. They had no solution either, but Freek told me to check the ResponseCache middleware and to see if I could replace the nonces there.

That's what I did, and after some regex problems (as usual), I found a way.

$response = $this->responseCache->getCachedResponseFor($request);
$csp = $response->headers->get('Content-Security-Policy');
$newCsp = preg_replace('/nonce-.+?(?=\')/', 'nonce-'.cspNonce(), $csp);
$response->headers->set('Content-Security-Policy', $newCsp);

Inside the package's middleware, I replaced the nonce from the cached header, with the new one. You can just use the global cspNonce() method to grab it. But that solved just the first part of the problem. Besides the header, the nonce is also used in the content of the response. I had to replace it there too. After another 30 minutes of regex trial and error, I came up with a solution.

$newContent = preg_replace('/(?<=nonce=")(.*)(?=")/', cspNonce(), $response->getContent());
$response->setContent($newContent);

This way, I was able to change the nonces in the content as well. It worked, and I was thrilled. There were no more CSP errors in the browser, and the nonces were updated. But right now, the changes happen in Spatie's package and not in my application. That's a problem.

Note: If you got better regex solutions for me, please contact me. With regex I always use the first one that works :-)

The Real Solution

I came up with an idea to use another custom middleware. The plan was to modify the header and content there after the Laravel ResponseCache package loaded the cached response. I wasn't sure if this would be even possible. The problem was the order of the middlewares. For me, it was obvious that they were processed in this order.

'web' => [
            \Spatie\ResponseCache\Middlewares\CacheResponse::class,
            \App\Http\Middleware\CacheControl::class,
            \App\Http\Middleware\Robots::class,
            SetReferrerPolicy::class,
            \Spatie\Csp\AddCspHeaders::class,
            UpdateHeaderAndContentNonces::class,
        ],

And they are for Before Middlewares. But it is the other way around, for middleware code that gets applied when the response leaves your application. You can log some text inside your middlewares to see when the code gets executed before and after the applications handles the request.

[2018-03-20 08:36:05] local.INFO: M1 before  
[2018-03-20 08:36:05] local.INFO: M2 before  
[2018-03-20 08:36:05] local.INFO: M3 before  
[2018-03-20 08:36:05] local.INFO: M3 after  
[2018-03-20 08:36:05] local.INFO: M2 after  
[2018-03-20 08:36:05] local.INFO: M1 after  

When you think about your middlewares as circles around your application, it makes sense. After leaving Middleware 3, the request is handled by the application and then gets back to M3, then M2 and finally M1. This was why I had to switch the order for my ones.

 'web' => [
	 UpdateHeaderAndContentNonces::class,
	 \Spatie\ResponseCache\Middlewares\CacheResponse::class,
	 \App\Http\Middleware\CacheControl::class,
	 \App\Http\Middleware\Robots::class,
	 SetReferrerPolicy::class,
	 \Spatie\Csp\AddCspHeaders::class,
 ],

Now I was able to make the nonce replacements in my UpdateHeaderAndContentNonces middleware.

public function handle($request, Closure $next)
{
    /** @var Response $response */
    $response = $next($request);

    $csp = $response->headers->get('Content-Security-Policy');
    $newCsp = preg_replace('/nonce-.+?(?=\')/', 'nonce-'.cspNonce(), $csp);
    $response->headers->set('Content-Security-Policy', $newCsp);

    $newContent = preg_replace('/(?<=nonce=")(.*)(?=")/', cspNonce(), $response->getContent());
    $response->setContent($newContent);

    return $response;
}

Conclusion

So this is flow now:

  • First request comes in
  • CSP headers are set
  • Response get cached
  • Second request comes in
  • Cached response will be loaded
  • Custom middleware updates the nonces

It would be possible to call the AddCspHeaders middleware for cached responses again, but I thought it would benefit the performance just to replace the nonces instead of creating all the CSP policies again.

In the end, I am glad I found a solution to make response caching and CSP nonces work side by side. I wouldn't want to kick one of them out.

Let's stay in touch

Sign up for my newsletter and I will let you know about more content and new projects of mine once a month.