HOWTO: Add Subresource Integrity Support to Your Theme

I want to share how I added Subresource Integrity (SRI) to my Hugo theme. First, a quick definition of SRI from the Mozilla Developer Network:

SRI is a security feature that enables browsers to verify that files they fetch (for example, from a CDN) are delivered without unexpected manipulation. It works by allowing you to provide a cryptographic hash that a fetched file must match.

The concept is very simple: you add a cryptographic hash to the script and link HTML tags of your theme’s JavaScript and CSS assets. When the user’s web browser loads the page it will fetch each asset and compute the hash locally to see if it matches the one specified on the page HTML. Note that this is only effective if the site serving your theme’s HTML is served over HTTPS, as otherwise an attacker — hostile coffee shop, airport, or government network, etc — could theoretically change the hash of assets on the page, negating the effect of using SRI in the first place.

If you want to see it in action you can visit picturingjordan.com and view the source code. Otherwise, keep reading for a technical breakdown of my implementation.

How it Works

My theme utilizes the NodeJS Package Manager (NPM) for dependency management and compilation. I wrote a script build/sri.js that reads a list of the theme’s assets from build/assets.json and computes their SHA-384 hashes. The hashes are outputted to data/sri.toml and Hugo inserts them into the theme’s HTML at build time using its custom Data Files support.

You can follow along below or go straight to the source code on GitHub.

The Code

First, the JavaScript that generates the hashes, build/sri.js:

var crypto = require('crypto');
var fs     = require('fs');
var assets = require('./assets.json');

var generate384 = function (file) {
  var enc  = 'utf8';
  var body = fs.readFileSync(file, { encoding: enc });
  var hash = crypto.createHash('sha384').update(body, enc);
  var sha  = hash.digest('base64');

  return 'sha384-' + sha;
}

for (var asset in assets) {
  var path = assets[asset];
  var hash = generate384(path);

  console.log(asset + ' = "' + hash + '"');
}

This script is called at theme build time by the NPM run scripts inside package.json, for example:

"generatesri": "node build/sri.js > data/sri.toml"

The script reads a list of assets used by the theme from build/assets.json:

{
  "style": "static/css/style.css",
  "cookieconsentcss": "static/css/cookieconsent.min.css",
  "cookieconsentjs": "static/js/cookieconsent.min.js"
}

The script outputs computed hashes for each asset to a data file, data/sri.toml:

style = "sha384-Cdt2yG10w21pA8DMpImJIvsLOME686p75OYD9jGCZVWvOol9zkEsaF3ctGEuBXK6"
cookieconsentcss = "sha384-6iYDyQZuuNT7DcPJGXx241czdv2+GDGUcXRiqw1iXrjgYMTorSetxFP3JCMQMwnR"
cookieconsentjs = "sha384-PDjg2ZdS3khPzd53i18+7tzB32JVQfFMrTXYo21RqPgUmEVAPwIhxOUF/8sP79CS"

This data file is read by Hugo during the building of the site, where it inserts the hashes into the generated HTML, ie layouts/_default/baseof.html:

<link href="{{ .Site.BaseURL }}css/style.css" rel="stylesheet" integrity="{{ .Site.Data.sri.style}}" crossorigin="anonymous">

The NodeJS build script was adapted from here. You can find implementations in other languages there as well. See the GitHub repository for my hugo-theme-bootstrap4-blog theme to see the implementation in full.

Thanks

Thanks! I hope this helps some developers implement this simple security feature into their themes. The Internet is becoming a scary place and we should help our users be safer.

7 Likes

Thanks for this - a superb solution for an ever increasingly important feature, in a CSS/JS Cryptomining world :slight_smile: