Skip to content

Commit

Permalink
Subresource integrity (#301)
Browse files Browse the repository at this point in the history
* add content integrity

* use sha512

* add content integrity checks

* fix hashes

* disable css integrity checks

* use toString() to cast buffers
  • Loading branch information
yoshuawuyts authored Oct 19, 2017
1 parent e547c00 commit 634ea38
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 22 deletions.
2 changes: 1 addition & 1 deletion lib/cmd-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 22 additions & 8 deletions lib/graph-document.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);'
var header = `<script>${content}</script>`
var base64 = sha512(content)
var header = `<script nomodule integrity="${base64}">${content}</script>`
return addToHead(header)
}

function scriptTransform (opts) {
var hash = opts.hash
var link = `/${hash}/bundle.js`
var header = `<script src="${link}" defer></script>`
var hex = opts.hash.toString('hex').slice(0, 16)
var base64 = 'sha512-' + opts.hash.base64Slice()
var link = `/${hex}/bundle.js`
var header = `<script src="${link}" defer integrity="${base64}"></script>`
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 = `<link rel="preload" as="style" href="${link}" onload="this.rel='stylesheet'">`
return addToHead(header)
}
Expand Down Expand Up @@ -174,7 +180,9 @@ function criticalTransform (opts) {
}

function reloadTransform (opts) {
var header = `<script>${opts.bundle}</script>`
var bundle = opts.bundle
var base64 = 'sha512-' + sha512(bundle)
var header = `<script integrity="${base64}">${bundle}</script>`
return addToHead(header)
}

Expand All @@ -201,3 +209,9 @@ function extractFonts (state) {

return res
}

function sha512 (buf) {
return 'sha512-' + crypto.createHash('sha512')
.update(buf)
.digest('base64')
}
4 changes: 2 additions & 2 deletions lib/graph-service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 28 additions & 10 deletions test/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -16,9 +18,9 @@ tape('renders some HTML', function (assert) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.polyfill.io/v2/polyfill.min.js" defer></script>
<script src="/__HASH__/bundle.js" defer></script>
<link rel="preload" as="style" href="/ebfdda3dbc9e925b/bundle.css" onload="this.rel='stylesheet'">
<script>;(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);;(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<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);</script>
<script src="/__SCRIPTS_HASH__/bundle.js" integrity="sha512-__SCRIPTS_INTEGRITY__" defer></script>
<link rel="preload" as="style" href="/__STYLE_HASH__/bundle.css" onload="this.rel='stylesheet'">
<script nomodule integrity="sha512-__PRELOAD_INTEGRITY__">;(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);;(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<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);</script>
<link rel="manifest" href="/manifest.json">
<meta name="description" content=>
<meta name="theme-color" content=#fff>
Expand Down Expand Up @@ -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__)
})
})
})

Expand All @@ -61,9 +71,9 @@ tape('server render choo apps', function (assert) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.polyfill.io/v2/polyfill.min.js" defer></script>
<script src="/__HASH__/bundle.js" defer></script>
<link rel="preload" as="style" href="/ebfdda3dbc9e925b/bundle.css" onload="this.rel='stylesheet'">
<script>;(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);;(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<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);</script>
<script src="/__SCRIPTS_HASH__/bundle.js" integrity="sha512-__SCRIPTS_INTEGRITY__" defer></script>
<link rel="preload" as="style" href="/__STYLE_HASH__/bundle.css" onload="this.rel='stylesheet'">
<script nomodule integrity="sha512-__PRELOAD_INTEGRITY__">;(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);;(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<b.length;c++){var d=b[c];"preload"===d.rel&&"style"===d.getAttribute("as")&&(a.loadCSS(d.href,d,d.getAttribute("media")),d.rel=null)}},!b.support()){b.poly();var c=a.setInterval(b.poly,300);a.addEventListener&&a.addEventListener("load",function(){b.poly(),a.clearInterval(c)}),a.attachEvent&&a.attachEvent("onload",function(){a.clearInterval(c)})}}})(this);</script>
<link rel="manifest" href="/manifest.json">
<meta name="description" content=>
<meta name="theme-color" content=#fff>
Expand Down Expand Up @@ -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__)
})
})
})

0 comments on commit 634ea38

Please sign in to comment.