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 | <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js" |
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 | <link href='https://code.angularjs.org/1.3.14/angular-csp.css' |
We need to generate the hashes. We can simply put any hash into the resource and Chrome will tell us SHA-265 value
1 | Failed to find a valid digest in the 'integrity' attribute for resource |
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 | curl -s https://code.angularjs.org/1.3.14/angular-csp.css | openssl dgst -sha384 -binary | openssl base64 -A |
It is simple to write a Bash function and place it in your profile or alias
file ~/.alias
1 | # compute SHA384 has of a given downloaded file |
Then we can simply pass URL and get the hash
1 | $ sha384 https://code.angularjs.org/1.3.14/angular-csp.css |
The above two resources thus become
1 | <link href="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-csp.css" |
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.