Subresource integrity for WordPress scripts and stylesheets

This post is more than 5 years old.

When we load scripts or stylesheets from other websites, we are inherently trusting those sites to deliver something safe. But what if they get hacked? Use subresource integrity to ensure authenticity and protect website visitors.

The problem

We all want our websites to be faster, because our visitors love faster websites. One way we achieve that is by loading commonly used scripts and stylesheets from a central place, a content distribution network or CDN, like Cloudflare’s CDNJS. The benefits include faster loading because those files can come from a server nearer the visitor; and the visitor’s browser might even have a copy of the file already in its cache.

But what happens if the CDN’s copy is damaged, either deliberately or by accident? This is not an academic problem, it has happened and has caused real problems for website owners:

Loading locally

A simple solution is to only load scripts and stylesheets from our local resource, the website itself. We have full control of that, and with good attention to security and code management, this should not be a problem. But remember that we’re using CDN-hosted resources for performance benefits, so there’s a trade-off we must accept if we’re going to do that. We can claw back that lost performance through a CDN… which is sounding eerily familiar!

Subresource Integrity

Modern browsers now have a feature for ensuring the integrity of scripts and stylesheets loaded in the browser. It’s called Subresource Integrity or SRI, and it works by comparing a cryptographic hash of a file’s contents with the expected hash in our web page. Here’s what it looks like for the classic Font Awesome v4.7.0 stylesheet:

<link rel="stylesheet"
 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
 integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0="
 crossorigin="anonymous" />

When a browser loads the file from CDNJS, it calculates the cryptographic hash and compares it to the expected hash in the integrity attribute of the link element. If the hashes don’t match, the browser won’t use that file.

NB: this works well for versioned resources, i.e. when you can request a specific, non-changing version of a resource. That’s true for many scripts and stylesheets, particularly ones that do things in your browser. It’s often not the case for services, which is well explained by Troy Hunt in his article, The JavaScript Supply Chain Paradox: SRI, CSP and Trust in Third Party Libraries.

Using SRI in WordPress

As at WordPress 5.0, there’s no native support for SRI. There’s an open ticket for adding SRI into the script loading framework in WordPress, but it could be a little while before it gets formalised and added into the core code.

In the meantime, here’s how to add SRI to specific external resources, the stylesheet for Font Awesome 4.7.0 and the script for Twemoji:

/**
* load custom CSS / JS required for fonts
*/
add_action('wp_enqueue_scripts', function() {
    wp_enqueue_style('font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', [], null);
    add_filter('style_loader_tag', __NAMESPACE__ . '\\add_font_awesome_sri', 10, 2);

    wp_enqueue_script('twemoji', 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/11.2.0/2/twemoji.min.js', [], null, true);
    add_filter('script_loader_tag', __NAMESPACE__ . '\\add_twemoji_sri', 10, 2);
    add_action('wp_print_footer_scripts', __NAMESPACE__ . '\\twemoji_script');
});

/**
* add SRI attributes to Font Awesome style loader element
* @param string $html
* @param string $handle
* @return string
*/
function add_font_awesome_sri($html, $handle) {
    if ($handle === 'font-awesome') {
        $html = str_replace(' />', ' integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />', $html);
    }

    return $html;
}

/**
* add SRI attributes to Twemoji script loader element
* @param string $html
* @param string $handle
* @return string
*/
function add_twemoji_sri($html, $handle) {
    if ($handle === 'twemoji') {
        $html = str_replace('></script>', ' integrity="sha256-AsQ6AskD2N30tG+vyEyJzpGgXIOQ0Z5AAFE7l+GmZ5s=" crossorigin="anonymous"></script>', $html);
    }

    return $html;
}

/**
* add script to initialise twemoji (Twitter Emoji)
*/
function twemoji_script() {
    echo '<script>twemoji.parse(document.body, { base: "https://cdnjs.cloudflare.com/ajax/libs/twemoji/11.2.0/2/", folder: "svg", ext: ".svg" });</script>';
}

/**
* #### Important! if you copy this code, please go to the source sites for the integrity hashes!
*/

If we want something a little more automatic, there’s even a WordPress plugin that adds SRI to your scripts for you.

Where do we find the integrity hash?

Good CDNs provide us with the hash when we get the URL for the file. For example, CDNJS has a drop-down list next to each file, from which we can choose to copy the integrity hash:

Copy the SRI hash for a file on CDNJS
Copy the SRI hash for a file on CDNJS

Font Awesome provides the integrity hash as well, when we use their CDN.

So now we can use a CDN-hosted script or stylesheet, and still ensure the integrity of the file and the safety of our website visitors.

What about Google web fonts?

Well… it’s messy. Google does a bunch of browser sniffing to determine the most efficient way to deliver fonts to each browser. In other words, you can get very different resource content sent to different browsers, which is exactly what SRI is trying to stop! So no SRI for Google web fonts, at least not for now.

What about IE11?

Internet Explorer doesn’t support SRI. :sad trombone: 😞

But let’s not let that stop us from making the Internet safer for people who update their web browsers.