Skip to content

Commit 634ea38

Browse files
authored
Subresource integrity (#301)
* add content integrity * use sha512 * add content integrity checks * fix hashes * disable css integrity checks * use toString() to cast buffers
1 parent e547c00 commit 634ea38

File tree

5 files changed

+54
-22
lines changed

5 files changed

+54
-22
lines changed

lib/cmd-build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function build (entry, opts) {
131131
function writeSingle (filename, type) {
132132
return function (err, node) {
133133
if (err) return log.error(err)
134-
var dirname = path.join(outdir, node.hash)
134+
var dirname = path.join(outdir, node.hash.toString('hex').slice(0, 16))
135135
mkdirp(dirname, function (err) {
136136
if (err) return log.error(err)
137137
filename = path.join(dirname, filename)

lib/graph-document.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var mapLimit = require('async-collection/map-limit')
22
var explain = require('explain-error')
33
var concat = require('concat-stream')
4+
var crypto = require('crypto')
45
var pump = require('pump')
56
var path = require('path')
67

@@ -106,21 +107,26 @@ function polyfillTransform () {
106107
function preloadTransform () {
107108
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);'
108109
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);'
109-
var header = `<script>${content}</script>`
110+
var base64 = sha512(content)
111+
var header = `<script nomodule integrity="${base64}">${content}</script>`
110112
return addToHead(header)
111113
}
112114

113115
function scriptTransform (opts) {
114-
var hash = opts.hash
115-
var link = `/${hash}/bundle.js`
116-
var header = `<script src="${link}" defer></script>`
116+
var hex = opts.hash.toString('hex').slice(0, 16)
117+
var base64 = 'sha512-' + opts.hash.base64Slice()
118+
var link = `/${hex}/bundle.js`
119+
var header = `<script src="${link}" defer integrity="${base64}"></script>`
117120
return addToHead(header)
118121
}
119122

120-
// TODO: make sure this works on browsers that don't support it.
123+
// NOTE: in theory we should be able to add integrity checks to stylesheets too,
124+
// but in practice it turns out that it conflicts with preloading. So it's best
125+
// to disable it for now. See:
126+
// https://twitter.com/yoshuawuyts/status/920794607314759681
121127
function styleTransform (opts) {
122-
var hash = opts.hash
123-
var link = `/${hash}/bundle.css`
128+
var hex = opts.hash.toString('hex').slice(0, 16)
129+
var link = `/${hex}/bundle.css`
124130
var header = `<link rel="preload" as="style" href="${link}" onload="this.rel='stylesheet'">`
125131
return addToHead(header)
126132
}
@@ -174,7 +180,9 @@ function criticalTransform (opts) {
174180
}
175181

176182
function reloadTransform (opts) {
177-
var header = `<script>${opts.bundle}</script>`
183+
var bundle = opts.bundle
184+
var base64 = 'sha512-' + sha512(bundle)
185+
var header = `<script integrity="${base64}">${bundle}</script>`
178186
return addToHead(header)
179187
}
180188

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

202210
return res
203211
}
212+
213+
function sha512 (buf) {
214+
return 'sha512-' + crypto.createHash('sha512')
215+
.update(buf)
216+
.digest('base64')
217+
}

lib/graph-service-worker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ function find (rootname, arr, done) {
114114
function fileEnv (state) {
115115
var script = [
116116
'https://cdn.polyfill.io/v2/polyfill.min.js',
117-
`${state.scripts.bundle.hash}/bundle.js`
117+
`${state.scripts.bundle.hash.toString('hex').slice(0, 16)}/bundle.js`
118118
]
119-
var style = [`${state.style.bundle.hash}/bundle.css`]
119+
var style = [`${state.style.bundle.hash.toString('hex').slice(0, 16)}/bundle.css`]
120120
var assets = split(state.assets.list.buffer)
121121
var doc = split(state.documents.list.buffer)
122122
var manifest = ['/manifest.json']

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"async-collection": "^1.0.1",
2222
"brfs": "^1.4.3",
2323
"browserify": "^14.4.0",
24-
"buffer-graph": "^3.0.0",
24+
"buffer-graph": "^4.0.0",
2525
"choo-log": "^7.2.1",
2626
"choo-reload": "^1.1.1",
2727
"clean-css": "^4.1.8",

test/document.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ var path = require('path')
66
var tape = require('tape')
77
var fs = require('fs')
88

9+
var __PRELOAD_INTEGRITY__ = 'ADDfrBcy5Z/jCgJsnxz75acy+CtquYdLuj+nu8nCaVZtvf9HI2TV08KKH3ZsSwYrkmfzEomyc626T8TlddpyiQ=='
10+
911
var bankai = require('../')
1012

1113
tape('renders some HTML', function (assert) {
@@ -16,9 +18,9 @@ tape('renders some HTML', function (assert) {
1618
<meta charset="utf-8">
1719
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1820
<script src="https://cdn.polyfill.io/v2/polyfill.min.js" defer></script>
19-
<script src="/__HASH__/bundle.js" defer></script>
20-
<link rel="preload" as="style" href="/ebfdda3dbc9e925b/bundle.css" onload="this.rel='stylesheet'">
21-
<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>
21+
<script src="/__SCRIPTS_HASH__/bundle.js" integrity="sha512-__SCRIPTS_INTEGRITY__" defer></script>
22+
<link rel="preload" as="style" href="/__STYLE_HASH__/bundle.css" onload="this.rel='stylesheet'">
23+
<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>
2224
<link rel="manifest" href="/manifest.json">
2325
<meta name="description" content=>
2426
<meta name="theme-color" content=#fff>
@@ -48,8 +50,16 @@ tape('renders some HTML', function (assert) {
4850
})
4951

5052
compiler.scripts('bundle.js', function (err, res) {
51-
expected = expected.replace('__HASH__', res.hash)
52-
assert.error(err, 'no error writing script')
53+
assert.ifError(err, 'no err bundling scripts')
54+
expected = expected.replace('__SCRIPTS_HASH__', res.hash.toString('hex').slice(0, 16))
55+
expected = expected.replace('__SCRIPTS_INTEGRITY__', res.hash.toString('base64'))
56+
57+
compiler.style(function (err, res) {
58+
assert.ifError(err, 'no err bundling style')
59+
expected = expected.replace('__STYLE_HASH__', res.hash.toString('hex').slice(0, 16))
60+
expected = expected.replace('__STYLE_INTEGRITY__', res.hash.toString('base64'))
61+
expected = expected.replace('__PRELOAD_INTEGRITY__', __PRELOAD_INTEGRITY__)
62+
})
5363
})
5464
})
5565

@@ -61,9 +71,9 @@ tape('server render choo apps', function (assert) {
6171
<meta charset="utf-8">
6272
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6373
<script src="https://cdn.polyfill.io/v2/polyfill.min.js" defer></script>
64-
<script src="/__HASH__/bundle.js" defer></script>
65-
<link rel="preload" as="style" href="/ebfdda3dbc9e925b/bundle.css" onload="this.rel='stylesheet'">
66-
<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>
74+
<script src="/__SCRIPTS_HASH__/bundle.js" integrity="sha512-__SCRIPTS_INTEGRITY__" defer></script>
75+
<link rel="preload" as="style" href="/__STYLE_HASH__/bundle.css" onload="this.rel='stylesheet'">
76+
<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>
6777
<link rel="manifest" href="/manifest.json">
6878
<meta name="description" content=>
6979
<meta name="theme-color" content=#fff>
@@ -103,7 +113,15 @@ tape('server render choo apps', function (assert) {
103113
})
104114

105115
compiler.scripts('bundle.js', function (err, res) {
106-
expected = expected.replace('__HASH__', res.hash)
107-
assert.error(err, 'no error writing script')
116+
assert.ifError(err, 'no err bundling scripts')
117+
expected = expected.replace('__SCRIPTS_HASH__', res.hash.toString('hex').slice(0, 16))
118+
expected = expected.replace('__SCRIPTS_INTEGRITY__', res.hash.toString('base64'))
119+
compiler.style(function (err, res) {
120+
assert.ifError(err, 'no err bundling style')
121+
assert.ifError(err)
122+
expected = expected.replace('__STYLE_HASH__', res.hash.toString('hex').slice(0, 16))
123+
expected = expected.replace('__STYLE_INTEGRITY__', res.hash.toString('base64'))
124+
expected = expected.replace('__PRELOAD_INTEGRITY__', __PRELOAD_INTEGRITY__)
125+
})
108126
})
109127
})

0 commit comments

Comments
 (0)