Dynamic JavaScript and SRI

by @edent | # # # | Read ~158 times.

Some external JavaScript libraries are dynamic. That's a problem for the SRI model of security. How can this be fixed?

Definitions

Suppose I want my website to have the latest version of the jQuery library. I might use a Content Delivery Network (CDN) to serve the code for me.

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

If an attacker were to get access to that CDN, they could inject code into my site and compromise my users' privacy.

This is not a theoretical problem. This is how British Airways and others were compromised. An attacker poisoned the external JS they were using and stole credit card details.

This is where SubResource Integrity (SRI) comes in. Rather than just linking to an external script, I will also take a cryptographic snapshot of it (a hash). If the code changes, it will no longer match that hash, and the browser will refuse to run the external code.

This is what it looks like:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" 
   integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
   crossorigin="anonymous"></script>

So far, so good.

Dynamic Resources

But not all external libraries are static. Take Polyfill.io - a clever service which dynamically returns code based on what browser is requesting it.
I've friends who work at the company which publishes Polyfill. These are my personal thoughts. I'm picking them because they're a popular service.

For example, if I include this script on my page:

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>

Firefox will get served the following JS:

(function(undefined) {}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});

But Opera will get served a completely different script:

(function(undefined) {Object.defineProperty(Array.prototype,"values",{value:Array.prototype[Symbol.iterator],enumerable:!1,writable:!1});var Iterator=function(){var e=function(){return this.length=0,this},t=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e},_=function(e,n){if(!(this instanceof _))r...

Because the contents are different, the SRI hashes will be different. That means we can't use the integrity attribute.

Solution?

As noted on Polyfill's GitHub page, the service...

sends a different content per User-Agent (and that's the point of the service), so for Subresource Integrity, website developers don't need a hash per URL, but per (URL, User-Agent) pair and they need to change the integrity attribute on a per UA basis

There's no sensible way to verify that the code being sent back hasn't been tampered with.

Conclusion

You should use SRI for all your external resources. If you can't, then you should either take a static copy and serve it yourself, or run your own dynamic service.

For example, The Guardian runs the Polyfill code on its own servers.
HTML source of The Guardian website. Polyfill is being loaded from their own CDN.

I trust the people at the Polyfill service not to act maliciously. And I trust them not to get hacked. But I shouldn't have to trust them. The same goes for any external service. Trust but verify.

Support this blog

Enjoyed this blog post? You can say thanks to the author in the following ways:

Donate to charity
Give to charity.
Buy me a birthday present
Amazon Wishlist
Get me a coffee
Donate on Ko-Fi.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.