How to use SRI hashes to secure third-party dependencies

Add a hash to make sure the CDN can only send the file you expect

Author's image
Tamás Sallai
5 mins

Security problems with loading scripts

Whenever a site loads a Javascript via a <script> tag, it gives it free rein on the page. It can read and modify the contents, make HTTP requests, store and read some cookies, access the local storage and local databases, and can use all features that the page has access to.

This is not a problem for scripts hosted on the same domain, but it gives the same enormous capabilities to scripts loaded from third-party websites.

For example, you might want to host jquery on cdnjs instead of your own domain. This script tag loads the library code from the remote server:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

Checking the site shows that jquery is loaded, and adding a simple script tag is definitely an easier option to setting up some sort of dependency management. But how do you know that cdnjs will always send jquery unmodified in the future? It might choose to include some malware, for example, a cryptocurrency miner script, an attack called cryptojacking (there is a detailed article on how that works here).

This is not a theoretical problem. In 2018 a plugin called Browsealoud was hacked and its script that was loaded on a ton of websites started mining cryptocurrency for the hackers. In this case, it was only for direct financial gains, but the injected script could do a lot more damage.

More recently, in 2020, Twilio suffered a similar attack where they left open their S3 bucket that hosted their script to be world-writable and somebody came and overwrote it. Every site that used that script was immediately compromised.

SRI hash

SRI (Subresource Integrity) is a feature that aims to solve this problem. It's been available since mid-2015 and gained universal support in early 2019. It works by adding an integrity tag to the resource which the browser checks during the download.

This is the jquery script with SRI added:

<script
	src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
	integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg=="
	crossorigin="anonymous"
></script>

The integrity is the hash of the content. When the browser downloads the file, it checks if it matches the hardcoded value and rejects the script if not. As a result, if the file does not match the expected content, it won't load and an error appears on the console:

Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js' with
computed SHA-256 integrity '9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0='.
The resource has been blocked.

Note that the reliability of your site is still tied to the CDN. If it is down or compromised, then the resource you need won't load, possibly affecting functionality. SRI hashes only help with stopping the attack.

How to use SRI hashes

Some CDNs, such as cdnjs automatically include the integrity attribute when you copy the script tag. This makes using it the default.

If you need to generate the hash for a resource, srihash.org is an easy-to-use site. Just drop the URL, click "Hash!" and copy-paste the resulting tag.

If you need to script it, use OpenSSL:

openssl dgst -sha384 -binary FILENAME.js | openssl base64 -A

The problem with SRI

In which cases you can't use the integrity attribute? When the resource is not immutable, specifying the hash breaks when there is an update. For example, if you use Google Analytics, you are likely to use the ga.js or the analytics.js script to load it on the page. These are not versioned files, and Google updates them from time-to-time.

While it's possible to download the file and host it on your server, effectively freezing it in a version, it is actively discouraged in the official documentation:

While it's possible to download the JavaScript file (gtag.js) to examine it, storing or serving the JavaScript file locally is not recommended.

Referencing the JavaScript file from Google's servers (i.e., https://www.googletagmanager.com/gtag/js) ensures that you get access to new features and product updates as they become available, giving you the most accurate data in your reports.

And who doesn't want new features and product updates as they become available?

But the problem is not the lack of "new features" but the missing guarantee that the old version of the script will work in the future. Since Google controls both the script and the analytics backend, it can make backward-incompatible changes. With jquery, it is guaranteed that a given version won't change. With a script like ga.js and analytics.js, there is no such guarantee.

And it's not unique to Google. When you include the Facebook SDK, it also needs you to do it as a mutable file, as well as a ton of other service providers.

These are similar problems to what I experienced with CSP, another security feature that gives websites more control over what the browser allows on the page.

The solution?

Unfortunately, there is none. If the provider does not provide an immutable, versioned file, you can't do anything about it. Either way, you'll introduce problems, either by potentially breaking functionality or by introducing supply-chain vulnerabilities.

Conclusion

SRI is a powerful tool to prevent a very real problem. With a simple tag property, you can make sure that if you include scripts from other sites, even if those sites are compromised you won't suffer a security incident.

But unfortunately, this only works for immutable resources. When a script file is expected to change, you can't pinpoint a fixed version. This makes SRI not possible in these cases.

November 27, 2020
In this article