From 634ea387655d47d0fd723843bd4526c23052be4e Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Thu, 19 Oct 2017 14:17:02 +0200 Subject: [PATCH] Subresource integrity (#301) * add content integrity * use sha512 * add content integrity checks * fix hashes * disable css integrity checks * use toString() to cast buffers --- lib/cmd-build.js | 2 +- lib/graph-document.js | 30 +++++++++++++++++++++-------- lib/graph-service-worker.js | 4 ++-- package.json | 2 +- test/document.js | 38 +++++++++++++++++++++++++++---------- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/lib/cmd-build.js b/lib/cmd-build.js index 8e79fd66..e09250fa 100644 --- a/lib/cmd-build.js +++ b/lib/cmd-build.js @@ -131,7 +131,7 @@ function build (entry, opts) { function writeSingle (filename, type) { return function (err, node) { if (err) return log.error(err) - var dirname = path.join(outdir, node.hash) + var dirname = path.join(outdir, node.hash.toString('hex').slice(0, 16)) mkdirp(dirname, function (err) { if (err) return log.error(err) filename = path.join(dirname, filename) diff --git a/lib/graph-document.js b/lib/graph-document.js index d9b5c69c..aa3d1731 100644 --- a/lib/graph-document.js +++ b/lib/graph-document.js @@ -1,6 +1,7 @@ var mapLimit = require('async-collection/map-limit') var explain = require('explain-error') var concat = require('concat-stream') +var crypto = require('crypto') var pump = require('pump') var path = require('path') @@ -106,21 +107,26 @@ function polyfillTransform () { function preloadTransform () { var content = ';(function(a){"use strict";var b=function(b,c,d){function e(a){return h.body?a():void setTimeout(function(){e(a)})}function f(){i.addEventListener&&i.removeEventListener("load",f),i.media=d||"all"}var g,h=a.document,i=h.createElement("link");if(c)g=c;else{var j=(h.body||h.getElementsByTagName("head")[0]).childNodes;g=j[j.length-1]}var k=h.styleSheets;i.rel="stylesheet",i.href=b,i.media="only x",e(function(){g.parentNode.insertBefore(i,c?g:g.nextSibling)});var l=function(a){for(var b=i.href,c=k.length;c--;)if(k[c].href===b)return a();setTimeout(function(){l(a)})};return i.addEventListener&&i.addEventListener("load",f),i.onloadcssdefined=l,l(f),i};"undefined"!=typeof exports?exports.loadCSS=b:a.loadCSS=b})("undefined"!=typeof global?global:this);' content += ';(function(a){if(a.loadCSS){var b=loadCSS.relpreload={};if(b.support=function(){try{return a.document.createElement("link").relList.supports("preload")}catch(b){return!1}},b.poly=function(){for(var b=a.document.getElementsByTagName("link"),c=0;c${content}` + var base64 = sha512(content) + var header = `` return addToHead(header) } function scriptTransform (opts) { - var hash = opts.hash - var link = `/${hash}/bundle.js` - var header = `` + var hex = opts.hash.toString('hex').slice(0, 16) + var base64 = 'sha512-' + opts.hash.base64Slice() + var link = `/${hex}/bundle.js` + var header = `` return addToHead(header) } -// TODO: make sure this works on browsers that don't support it. +// NOTE: in theory we should be able to add integrity checks to stylesheets too, +// but in practice it turns out that it conflicts with preloading. So it's best +// to disable it for now. See: +// https://twitter.com/yoshuawuyts/status/920794607314759681 function styleTransform (opts) { - var hash = opts.hash - var link = `/${hash}/bundle.css` + var hex = opts.hash.toString('hex').slice(0, 16) + var link = `/${hex}/bundle.css` var header = `` return addToHead(header) } @@ -174,7 +180,9 @@ function criticalTransform (opts) { } function reloadTransform (opts) { - var header = `` + var bundle = opts.bundle + var base64 = 'sha512-' + sha512(bundle) + var header = `` return addToHead(header) } @@ -201,3 +209,9 @@ function extractFonts (state) { return res } + +function sha512 (buf) { + return 'sha512-' + crypto.createHash('sha512') + .update(buf) + .digest('base64') +} diff --git a/lib/graph-service-worker.js b/lib/graph-service-worker.js index 9f082067..6267f7e0 100644 --- a/lib/graph-service-worker.js +++ b/lib/graph-service-worker.js @@ -114,9 +114,9 @@ function find (rootname, arr, done) { function fileEnv (state) { var script = [ 'https://cdn.polyfill.io/v2/polyfill.min.js', - `${state.scripts.bundle.hash}/bundle.js` + `${state.scripts.bundle.hash.toString('hex').slice(0, 16)}/bundle.js` ] - var style = [`${state.style.bundle.hash}/bundle.css`] + var style = [`${state.style.bundle.hash.toString('hex').slice(0, 16)}/bundle.css`] var assets = split(state.assets.list.buffer) var doc = split(state.documents.list.buffer) var manifest = ['/manifest.json'] diff --git a/package.json b/package.json index c1ea659f..48dc0667 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "async-collection": "^1.0.1", "brfs": "^1.4.3", "browserify": "^14.4.0", - "buffer-graph": "^3.0.0", + "buffer-graph": "^4.0.0", "choo-log": "^7.2.1", "choo-reload": "^1.1.1", "clean-css": "^4.1.8", diff --git a/test/document.js b/test/document.js index 457eea96..b9eecfa2 100644 --- a/test/document.js +++ b/test/document.js @@ -6,6 +6,8 @@ var path = require('path') var tape = require('tape') var fs = require('fs') +var __PRELOAD_INTEGRITY__ = 'ADDfrBcy5Z/jCgJsnxz75acy+CtquYdLuj+nu8nCaVZtvf9HI2TV08KKH3ZsSwYrkmfzEomyc626T8TlddpyiQ==' + var bankai = require('../') tape('renders some HTML', function (assert) { @@ -16,9 +18,9 @@ tape('renders some HTML', function (assert) { - - - + + + @@ -48,8 +50,16 @@ tape('renders some HTML', function (assert) { }) compiler.scripts('bundle.js', function (err, res) { - expected = expected.replace('__HASH__', res.hash) - assert.error(err, 'no error writing script') + assert.ifError(err, 'no err bundling scripts') + expected = expected.replace('__SCRIPTS_HASH__', res.hash.toString('hex').slice(0, 16)) + expected = expected.replace('__SCRIPTS_INTEGRITY__', res.hash.toString('base64')) + + compiler.style(function (err, res) { + assert.ifError(err, 'no err bundling style') + expected = expected.replace('__STYLE_HASH__', res.hash.toString('hex').slice(0, 16)) + expected = expected.replace('__STYLE_INTEGRITY__', res.hash.toString('base64')) + expected = expected.replace('__PRELOAD_INTEGRITY__', __PRELOAD_INTEGRITY__) + }) }) }) @@ -61,9 +71,9 @@ tape('server render choo apps', function (assert) { - - - + + + @@ -103,7 +113,15 @@ tape('server render choo apps', function (assert) { }) compiler.scripts('bundle.js', function (err, res) { - expected = expected.replace('__HASH__', res.hash) - assert.error(err, 'no error writing script') + assert.ifError(err, 'no err bundling scripts') + expected = expected.replace('__SCRIPTS_HASH__', res.hash.toString('hex').slice(0, 16)) + expected = expected.replace('__SCRIPTS_INTEGRITY__', res.hash.toString('base64')) + compiler.style(function (err, res) { + assert.ifError(err, 'no err bundling style') + assert.ifError(err) + expected = expected.replace('__STYLE_HASH__', res.hash.toString('hex').slice(0, 16)) + expected = expected.replace('__STYLE_INTEGRITY__', res.hash.toString('base64')) + expected = expected.replace('__PRELOAD_INTEGRITY__', __PRELOAD_INTEGRITY__) + }) }) })