I’ve had this exact same battle.
Obviously the intended way to use nonces in a Content-Security-Policy
is to have the nonce generated, injected, and served in a single operation. So in PHP,
perhaps, you might do something like this:
<?php $nonce = bin2hex(random_bytes(16)); header("Content-Security-Policy: script-src 'nonce-$nonce'"); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>PHP CSP Nonce Test</title> </head> <body> <h1>PHP CSP Nonce Test</h1> <p> JavaScript did not run. </p> <!-- This JS has a valid nonce: --> <script nonce="<?php echo $nonce; ?>"> document.querySelector('p').textContent = 'JavaScript ran successfully.'; </script> <!-- This JS does not: --> <script nonce="wrong-nonce"> alert('The bad guys won!'); </script> </body> </html>
But for folks like me – and you too, Vika,, from the sounds of things – who serve most of their pages, most of the time, from the cache or from static HTML files… and who add the CSP header on using webserver configuration… this approach just doesn’t work.
I experimented with a few solutions:
-
A long-lived nonce that rotates.
CSP allows you to specify multiple nonces, so I considered having a rotating nonce that was applied to pages (which were then cached for a period) and delivered by the header… and then a few hours later a new nonce would be generated and used for future page generations and appended to the header… and after the cache expiry time the oldest nonces were rotated-out of the header and became invalid. -
Dynamic nonce injection.
I experimented with having the webserver parse pages and add nonces: randomly generating a nonce, putting it in the header, and then basically doing as/<script/<script nonce="..."/
to search-and-replace it in.
Both of these are terrible solutions. The first one leaves a window of, in my case, about 24 hours during which a successfully-injected script can be executed. The second one effectively allowlists all scripts, regardless of their provenance. I realised that what I was doing was security theatre: seeking to boost my A-rating to an A+-rating on SecurityHeaders.com without actually improving security at all.
But the second approach gave me an idea. I could have a server-side secret that gets search-replaced out. E.g. if I “signed” all of my legitimate scripts with something like
<script nonce="dans-secret-key-goes-here" ...>
then I could replace s/dans-secret-key-goes-here/actual-nonce-goes-here/
and thus have the best of both
worlds: static, cacheable pages, and actual untamperable nonces. So long as I took care to ensure that the pages were never delivered to anybody with the secret key still
intact, I’d be sorted!
Alternatively, I was looking into whether Caddy can do something like mod_asis does for Apache: that is, serve a file “as is”, with headers included in the file. That way, I could have the CSP header generated with the page and then saved into the cache, so it’s delivered with the same none every time… until the page changes. I’d love more webservers to have an “as is” mode, but I appreciate that might be a big ask (Apache’s mechanism, I suspect, exploits the fact that HTTP/1.0 and HTTP/1.1 literally send headers, followed by two CRLFs, then content… but that’s not what happens in HTTP/2+).
So yeah, I’ll probably do a server-side-secret approach, down the line. Maybe that’ll work for you, too.