I've talked many times about CSP and CSP nonces, the easy way to control JavaScript on your page, but someone recently pointed out an area we could improve. Report URI needed to increase the amount of entropy within our CSP nonces and here's how we did it.
Content Security Policy
Regular readers will need no introduction to CSP, a powerful application security defence mechanism that I've talked about extensively. CSP nonces are a feature within CSP that allows you to tackle serious problems like Cross-Site Scripting (XSS) in a much easier way. I've talked about how we do CSP nonces the easy way with Cloudflare Workers and how Report URI is now using CSP nonces in an enforced policy, and it's our use of CSP nonces that drew some attention.
Hack Yourself First
For those not familiar with the Hack Yourself First Workshop, it's a 2-day, hands-on workshop tackling some pretty serious application security issues. We spend most of our time hacking into a demo application and walking through the remediation steps. Originally created by Troy Hunt, I've since teamed up with him and even found genuine security issues in other applications during the workshop. There's also that time that someone hacked a car...
I love delivering these workshops because, alongside getting to play around with security issues for 2 days, I get to meet awesome people at various organisations and hear from them about what's actually impacting them. Whilst working through the XSS section and demonstrating how CSP and CSP nonces could save you, it was during a recent delivery of the workshop that someone noticed something.
There's a pattern
As Report URI is using an enforced CSP with nonces, I often demo the site as I can also show how we do it and provide more detailed answers to questions. It was during this demo section that a couple of attendees, Jon Are Rakvåg from SpareBank 1 and Johannes Aaram Ringstad from Fremtind, noticed that there was a pattern emerging in our CSP nonces across page loads. It didn't take too many refreshes of the page to see that they were right. I fired up Burp and grabbed 1,000 CSP nonces from our home page. Here's the first 20 and I think you can already see the pattern emerging.
MjAyMzEyNjgwMiwxNjc4MTQyMjk1
MzI2OTk1NzE3LDEyMDEyOTE1MTQ=
ODY4MjgwNDIxLDQ0ODU3MDk5
OTg5NjQ4NjIsNDAxNDI2ODA2Mw==
MjIxMjkyMTk5OCwyMDkwNTc1Njc5
OTc2ODQ4OTkyLDI3ODUwNTkxNjA=
MTkxNjk1NjI2MiwxMjYxMzI4NTEw
MzEyNjI3ODQ2OCwyMTE0MDc3NDY5
MzQ0ODAwNDIxNCw4OTg5MDk4MTI=
MjkxMTI5NjE3NiwxNTA5NTk0NDQ3
MjE5Njg1NTg0LDI4NzQ0MjU4NTU=
MjY0Nzk5OTMyOCwyMDc1ODc4MzI4
NzI2NjU5MjI2LDUxNDY3OTQ3NA==
NjA3OTczNDc0LDE3MDE4MzY3NDI=
MzAzNjA4MTU2OSw1MTUwNjUwNzM=
MzEwNDU2MTgwNCwzMzIyODkxOTQ2
NDE2ODcwNjkxNCwyMzE4MjM5OTI4
MjgxOTczNTM3LDExMTQzNjY5MDE=
Mzg2NTg4NTMwOCwyNDc2NjY5NzMx
MjIwODc3NjEzLDE5MDUzMTM4MTM=
...
Looking at our code to see how we generate our CSP nonce values, remembering we do this in a Cloudflare Worker, you can see where the pattern is coming from.
let cspNonce = btoa(crypto.getRandomValues(new Uint32Array(2)))
I have a demo worker here in the playground that you can try out, but the problem is that we're base64 encoding a numeric value. This means that, due to how close together values are in ASCII, we can explain the pattern at the beginning of the nonce.
btoa(0) >> MA==
btoa(1) >> MQ==
btoa(2) >> Mg==
btoa(3) >> Mw==
btoa(4) >> NA==
btoa(5) >> NQ==
btoa(6) >> Ng==
btoa(7) >> Nw==
btoa(8) >> OA==
btoa(9) >> OQ==
Here's a worker to demo it, but the base64 is basically saying 'this always starts with a digit', which is correct. We did go a little bit deeper though, because it was a room full of technical people and we love geeking out on this stuff!
Jon showed that using Burp we could grab a selection of nonces and do some analysis on them. I took this one step further and retrieved 1,000 nonces, just to make sure we had a good sample, and ran the same analysis which I'm providing here. It seems that Burp was estimating we only had 38 bits of entropy in our nonces, when the specification recommends 128 bits.
What was even more interesting was looking at how much entropy there was for each bit and character in the nonce itself. There's a low value for the start of the string given the base64 pattern explained above, and that Burp pads tokens that are shorter than the expected size. There's also a lack of entropy in the middle of the string where the comma separates the two different 32bit numbers.
Just to really drive the final nail into the coffin, we looked at the Shannon Entropy score using CyberChef.
It's safe to say that the observation was 100% correct, and we didn't really need to do any investigation to confirm that. With regards to the problem itself, we have to look at how nonces work to understand if this presents a real issue, but for me, we were going to fix this regardless. With ~38bits of entropy in the nonce I doubt that any attacker could leverage this in a useful way but that's not the point, it shouldn't be this way and it's an easy fix, so we fixed it!
Moar entropy!!
After a brief discussion on the issue, we had a ticket open and Michal got to work on improving our nonce generation code. It took only a matter of minutes for him to have the PR up and ready for review, with the new nonce generation code looking like this.
let cspNonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(18))))
I setup a worker in the playground here so you can see the new code in action but the trick is the String.fromCharCode
. As we were previously base64 encoding only digits, there were only 10 possible values that could be present in the string, 0-9. Now that we're switching to characters from the Uint8
there are 255 possible characters that could be present in the string. Michal also switched us up to using "18 bytes because 16 bytes would have two trailing ==
, so we can as well use those positions." As you can see in a sample of 1,000 nonces using the new code, things are looking much better! Here's the top 20.
BDhAVojs+7iEm3+DAqHKq46Q
h7uz80W6lVI0PBIm0eFlknHz
7MebZuin0MSs4YQ8jPouE7qp
jmePb9jmmAa+j6OAGpYrnyP/
R+5sZ2sqLR42j7B1nHAl5Qhr
9C2YzbEmoqrKeF5gtUi48ho/
Oz3G01zsds1AxdA+0fGckBRK
0W6O3gljkpP6VufyEWoJOtej
z5l9SIyyUrrueIvO+ZU0Qx7b
/sko5Xx9dNRNOlLxWg6w9sTg
DCX3xK5Gz701q9OJ7giJRDqL
TRC84ckRhWHjwrv0i7eir9i9
DS7N8/SHf2n6A5IGgEDbnESr
14FsB/jkGHceQq9g6wWkgYxP
wnv9TBha4IO0qyrHxLUGdOdn
wkjeNrXSXGsjS/oDe5XoCV7h
5iCtSvW3YcF8Lgd94VJV+W2A
RDXVLLD9Hv7jAjw02q884vhr
AoZjd8SZiyGFO101aJLSq5Ht
ns/8whJrS6n/iYMqiz0Q0Czr
...
I also ran the analysis again in Burp and things are looking considerably better with ~130bits of entropy estimated from our 144bits.
We also managed to improve the entropy of all bits across the nonce as we no longer have the common starting characters and the comma in the middle of the string.
Finally, the Shannon Entropy now seems much better and more aligned with what we'd like!
You can now head over to Report URI and view the source on our page to see the CSP nonce, or check the header itself, and observe our newer, more random nonces!