Subresource integrity (SRI)

How to use hashes for CDN resources.

Modern browsers (Chrome, Firefox) can check if the script or style file downloaded from a CDN has the expected content. This is done by specifying a hash attribute inside the script tag.

1
2
3
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"
integrity="sha384-I6F5OKECLVtK/BL+8iSLDEHowSAfUo76ZL9+kGAgTRdiByINKJaqTPH/QVNS1VDb"
crossorigin="anonymous"></script>

The browser downloads the script file from the CloudFlare CDN, computes the SHA384 hash (this is quick), and then compares the value with the attribute value "sha384-I6F5OKE...". If the computed value for the downloaded file is different from the listed attribute, the script does not run.

This is a great security feature, and allows you to offload common libraries to 3rd party servers (CDN) without compromising your security. I recommend reading this excellent post to learn more about SRI and hashes.

How

To use SRI you need to compute the hashes. Open the page that loads resources from CDN. For example, it might use AngularJS.

1
2
3
4
5
<link href='https://code.angularjs.org/1.3.14/angular-csp.css'
rel="stylesheet" type="text/css">

<script
src='https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js'>
</script>

We need to generate the hashes. We can simply put any hash into the resource and Chrome will tell us SHA-265 value

1
2
3
4
Failed to find a valid digest in the 'integrity' attribute for resource
'https://fonts.googleapis.com/css?family=Montserrat:400,700' with computed
SHA-256 integrity 'vVtP3iXwU8kbMQMRxuvR8KjRkPyJjPBhGjgMApNcRWo='.
The resource has been blocked.

You can also use the online SRI hash generator.

I prefer using longer hash SHA384 and generate them quickly from the terminal. The best way to generate hashes from command line is to pipe the downloaded script into openssl tool.

1
2
curl -s https://code.angularjs.org/1.3.14/angular-csp.css | openssl dgst -sha384 -binary | openssl base64 -A
w6jkqTtaZl4DoSpc0dHU2JsugQVM3zFUnN7I6hMqV5DvJ6s1hbwWDwHUGBRfCESQ

It is simple to write a Bash function and place it in your profile or alias file ~/.alias

1
2
3
4
5
6
7
# compute SHA384 has of a given downloaded file
function sha384() {
curl -s $1 | openssl dgst -sha384 -binary | openssl base64 -A | pbcopy
echo Copied SHA384 into your clipboard
pbpaste
echo
}

Then we can simply pass URL and get the hash

1
2
3
$ sha384 https://code.angularjs.org/1.3.14/angular-csp.css
Copied SHA384 into your clipboard
w6jkqTtaZl4DoSpc0dHU2JsugQVM3zFUnN7I6hMqV5DvJ6s1hbwWDwHUGBRfCESQ

The above two resources thus become

1
2
3
4
5
6
7
<link href="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-csp.css"
integrity="sha384-w6jkqTtaZl4DoSpc0dHU2JsugQVM3zFUnN7I6hMqV5DvJ6s1hbwWDwHUGBRfCESQ"
crossorigin="anonymous"
rel="stylesheet" type="text/css">

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"
integrity="sha384-nl4Gepde8OWprDnbAi0u6kXyDcHpiL0V1bI6qKzUqlgQZ5OAfeFvFiuT8fACkoL2"
crossorigin="anonymous"></script>

Note 1: when requesting a CDN resource, and assuming it can be compromised, as we do now, it is important NOT to send cookies and headers. Thus the resource tag has crossorigin="anonymous" attribute. Not every CDN supports it. For example, code.angularjs.org/1.3.14/angular-csp.css will not return the cross-origin header with the file. You should just switch to another CDN in this case, for example to ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-csp.css or cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-csp.css.

Note 2: you can specify multiple hashes, separated by a white space. This is useful for example to specify hashes longer than the browser supports. sha256-... sha384-... sha512-.... The browser will pick the longest hash it supports to check.

Note 3: for some resources, like Google Fonts, the SHA I computed was always different from the computed Chrome resource. For example, my SHA256 computation for https://fonts.googleapis.com/css?family=Montserrat:400,700 produces GfXXB3eEVJVsy6tSoUxaGJtbN+NZQdwmzMXe//MYdXw= while Chrome reports vVtP3iXwU8kbMQMRxuvR8KjRkPyJjPBhGjgMApNcRWo=. Seems Google fonts service returns a different CSS optimized for Google Chrome if this browser is requesting it. To handle such cases, you can specify alternative hashes, but make sure to specify them at every hash length. For example, if we specify alternatives at SHA256, and a single SHA384, the shorter alternatives will be ignored, because the browser only looks at the longest hashes.

Note 4: You can find the SHA computation scripts in my alias file

Note 5: Unfortunately, there is no way to enforce every CDN resource to have a hash, unless you write your own tooling to parse the pages and check the links. For now I would rely on code reviews to catch new CDN resources without hashes.