Skip to content
This repository has been archived by the owner on Sep 30, 2021. It is now read-only.

Commit

Permalink
[FIXES #7] HTML should invalidate if any potential SRI input invalidates
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanpenner committed Nov 17, 2015
1 parent ebffe5b commit 75c4b42
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 62 deletions.
1 change: 0 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"extends": "nightmare-mode",
"env": {
"node": true
},
Expand Down
9 changes: 0 additions & 9 deletions Brocfile.js

This file was deleted.

96 changes: 54 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ var CachingWriter = require('broccoli-caching-writer');
var sriToolbox = require('sri-toolbox');
var fs = require('fs');
var crypto = require('crypto');
var styleCheck = /\srel=["\'][^"]*stylesheet[^"]*["\']/;
var srcCheck = /\ssrc=["\']([^"\']+)["\']/;
var hrefCheck = /\shref=["\']([^"\']+)["\']/;
var symlinkOrCopy = require('symlink-or-copy').sync;
var Promise = require('rsvp').Promise; // node 0.10
var path = require('path');

var STYLE_CHECK = /\srel=["\'][^"]*stylesheet[^"]*["\']/;
var SRC_CHECK = /\ssrc=["\']([^"\']+)["\']/;
var HREF_CHECK = /\shref=["\']([^"\']+)["\']/;
var SCRIPT_CHECK = new RegExp('<script[^>]*src=["\']([^"]*)["\'][^>]*>', 'g');
var LINT_CHECK = new RegExp('<link[^>]*href=["\']([^"]*)["\'][^>]*>', 'g');
var INTEGRITY_CHECK = new RegExp('integrity=["\']');
var CROSS_ORIGIN_CHECK = new RegExp('crossorigin=["\']([^"\']+)["\']');
var MD5_CHECK = /^(.*)[-]([a-z0-9]{32})([.].*)$/;

function SRIHashAssets(inputNodes, options) {
if (!(this instanceof SRIHashAssets)) {
Expand All @@ -21,9 +29,9 @@ function SRIHashAssets(inputNodes, options) {

CachingWriter.call(this, nodes, {
cacheInclude: [
/(.*)\.html$/,
/(.*)\.js$/,
/(.*)\.css$/
/\.html$/,
/\.js$/,
/\.css$/
]
});

Expand All @@ -40,18 +48,12 @@ function SRIHashAssets(inputNodes, options) {

SRIHashAssets.prototype = Object.create(CachingWriter.prototype);
SRIHashAssets.prototype.constructor = SRIHashAssets;
/*
SRIHashAssets.prototype.extensions = ['html'];
SRIHashAssets.prototype.targetExtension = 'html';
*/

SRIHashAssets.prototype.addSRI = function addSRI(string, file) {
var that = this;
var scriptCheck = new RegExp('<script[^>]*src=["\']([^"]*)["\'][^>]*>', 'g');
var linkCheck = new RegExp('<link[^>]*href=["\']([^"]*)["\'][^>]*>', 'g');
SRIHashAssets.prototype.addSRI = function addSRI(string, srcDir) {
var plugin = this;

return string.replace(scriptCheck, function srcMatch(match) {
var src = match.match(srcCheck);
return string.replace(SCRIPT_CHECK, function srcMatch(match) {
var src = match.match(SRC_CHECK);
var filePath;

if (!src) {
Expand All @@ -60,20 +62,19 @@ SRIHashAssets.prototype.addSRI = function addSRI(string, file) {

filePath = src[1];

return that.mungeOutput(match, filePath, file);
}).replace(linkCheck, function hrefMatch(match) {
var href = match.match(hrefCheck);
var isStyle = styleCheck.test(match);
return plugin.mungeOutput(match, filePath, srcDir);
}).replace(LINT_CHECK, function hrefMatch(match) {
var href = match.match(HREF_CHECK);
var isStyle = STYLE_CHECK.test(match);
var filePath;


if (!isStyle || !href) {
return match;
}

filePath = href[1];

return that.mungeOutput(match, filePath, file);
return plugin.mungeOutput(match, filePath, srcDir);
});
};

Expand All @@ -85,6 +86,7 @@ SRIHashAssets.prototype.readFile = function readFile(dirname, file) {
} catch(e) {
return null;
}

return assetSource;
};

Expand Down Expand Up @@ -114,7 +116,6 @@ SRIHashAssets.prototype.paranoiaCheck = function paranoiaCheck(assetSource) {
};

SRIHashAssets.prototype.generateIntegrity = function generateIntegrity(output, file, dirname, external) {
var crossoriginCheck = new RegExp('crossorigin=["\']([^"\']+)["\']');
var assetSource = this.readFile(dirname, file);
var selfCloseCheck = /\s*\/>$/;
var integrity;
Expand All @@ -137,7 +138,7 @@ SRIHashAssets.prototype.generateIntegrity = function generateIntegrity(output, f
append = ' integrity="' + integrity + '"';

if (external && this.options.crossorigin) {
if (!crossoriginCheck.test(output)) {
if (!CROSS_ORIGIN_CHECK.test(output)) {
append = append + ' crossorigin="' + this.options.crossorigin + '" ';
}
}
Expand All @@ -151,8 +152,7 @@ SRIHashAssets.prototype.generateIntegrity = function generateIntegrity(output, f
};

SRIHashAssets.prototype.checkExternal = function checkExternal(output, file, dirname) {
var md5Check = /^(.*)[-]([a-z0-9]{32})([.].*)$/;
var md5Matches = file.match(md5Check);
var md5Matches = file.match(MD5_CHECK);
var md5sum = crypto.createHash('md5');
var assetSource;
var filePath;
Expand All @@ -175,42 +175,54 @@ SRIHashAssets.prototype.checkExternal = function checkExternal(output, file, dir
return output;
}
}

md5sum.update(assetSource);
if (md5Matches[2] === md5sum.digest('hex')) {
return this.generateIntegrity(output, filePath, dirname, true);
}
return output;
};

SRIHashAssets.prototype.mungeOutput = function mungeOutput(output, filePath, file) {
var integrityCheck = new RegExp('integrity=["\']');
SRIHashAssets.prototype.mungeOutput = function mungeOutput(output, filePath, srcDir) {
var newOutput = output;

if (/^https?:\/\//.test(filePath)) {
return this.checkExternal(output, filePath, file);
return this.checkExternal(output, filePath, srcDir);
}
if (!(integrityCheck.test(output))) {
newOutput = this.generateIntegrity(output, filePath, file);

if (!INTEGRITY_CHECK.test(output)) {
newOutput = this.generateIntegrity(output, filePath, srcDir);
}
return newOutput;
};

SRIHashAssets.prototype.processFile = function processFile(srcDir, destDir, relativePath) {
var fileContent = fs.readFileSync(srcDir + '/' + relativePath);
var that = this;
SRIHashAssets.prototype.processHTMLFile = function processFile(entry) {
var srcDir = path.dirname(entry.fullPath);
var fileContent = this.addSRI(fs.readFileSync(entry.fullPath,'UTF-8'), srcDir);

this._srcDir = srcDir;
fileContent = this.addSRI(fileContent.toString(), srcDir);

return Promise.resolve().then(function writeFileOutput() {
var outputPath = that.getDestFilePath(relativePath);
fs.writeFileSync(this.outputPath + '/' + entry.relativePath, fileContent);
};

fs.writeFileSync(destDir + '/' + outputPath, fileContent);
});
SRIHashAssets.prototype.processOtherFile = function(entry) {
symlinkOrCopy(entry.fullPath, this.outputPath + '/' + entry.relativePath);
};

SRIHashAssets.prototype.build = function () {
// TODO call processFile here this.listEntries();
var html = [];
var other = [];

this.listEntries().forEach(function(entry) {
if (/\.html$/.test(entry.relativePath)) {
html.push(entry);
} else {
other.push(entry);
}
});

return Promise.all([
Promise.all(html.map(this.processHTMLFile.bind(this))),
Promise.all(other.map(this.processOtherFile.bind(this)))
]);
};

module.exports = SRIHashAssets;
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"main": "index.js",
"scripts": {
"lint": "eslint index.js",
"test": "snyk test && rm -rf tmp && mkdir tmp && npm run lint > tmp/lint-out && broccoli build tmp/output && mocha"
"test": "snyk test && mocha test",
"test:fast": "mocha test",
"test:debug": "mocha debug test"
},
"author": "Jonathan Kingston",
"repository": {
Expand All @@ -18,13 +20,16 @@
"broccoli-caching-writer": "^2.2.0",
"rsvp": "^3.0.0",
"snyk": "^1.1.0",
"sri-toolbox": "^0.2.0"
"sri-toolbox": "^0.2.0",
"symlink-or-copy": "^1.0.1"
},
"devDependencies": {
"broccoli": "^0.16.8",
"broccoli-cli": "^1.0.0",
"chai": "^3.4.1",
"eslint": "^1.9.0",
"eslint-config-nightmare-mode": "0.3.0",
"mocha": "^2.3.4"
"mocha": "^2.3.4",
"mocha-eslint": "^1.0.0"
}
}
30 changes: 30 additions & 0 deletions test/fixtures/output2/test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- styles -->
<link rel="stylesheet" href="https://example.com/thing.css">
<link rel="stylesheet" href="https://example.com/thing.css">
<link rel="stylesheet" href="vendor.css" />
<link rel="stylesheet" href="other.css" integrity="sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk= sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==" />
<link rel="stylesheet" hresf="other.css" />
<link href="other.css" />
<linkhref="other.css" rel="stylesheet" />
<link h="other.css" rel="stylesheet" />
<link hsref="other.css" rel="stylesheet" />
<link href="other.css" rel="stylesheet" integrity="sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk= sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==" />
<link href="other.css" rel="stylesheet" integrity="sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk= sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==" >
<link href="other.css" rel="stylesheet" integrity="sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk= sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==" >
<link rel="stylesheet" href="https://example.com/vendor.css" />
<link rel="stylesheet" href="https://subdomain.cloudfront.net/assets/vendor-d41d8cd98f00b204e9800998ecf8427e.css">

<!-- scripts -->
<script src="thing.js" integrity="sha256-oFeuE/P+XJMjkMS5pAPudQOMGJQ323nQt+DQ+9zbdAg= sha512-+EXjzt0I7g6BjvqqjkkboGyRlFSfIuyzY2SQ43HQKZBrHsjmRzEdjSHhiDzVs30nXL9H0tKw6WbMPc6RfzUumQ==" ></script>
<script src="moment-with-locales.min.js"></script>
<script src="unicode-chars.js"></script>
<script srsc="unicode-chars.js"></script>
<script s="unicode-chars.js"></script>
<script src="https://example.com/thing-5e1978f9cfa158d9841d7b6d8a4e5c57.js" integrity="sha256-oFeuE/P+XJMjkMS5pAPudQOMGJQ323nQt+DQ+9zbdAg= sha512-+EXjzt0I7g6BjvqqjkkboGyRlFSfIuyzY2SQ43HQKZBrHsjmRzEdjSHhiDzVs30nXL9H0tKw6WbMPc6RfzUumQ==" crossorigin="anonymous" ></script>
<script src="https://example.com/thing-5e1978f9cfa158d9841d7b6d8a4e5c57.js" integrity="sha256-oFeuE/P+XJMjkMS5pAPudQOMGJQ323nQt+DQ+9zbdAg= sha512-+EXjzt0I7g6BjvqqjkkboGyRlFSfIuyzY2SQ43HQKZBrHsjmRzEdjSHhiDzVs30nXL9H0tKw6WbMPc6RfzUumQ==" crossorigin="anonymous" ></script>
<script src="https://example.com/thing-5e1978f9cfa158d9841d7b6d8a4e5c57.js" crossorigin="use-credentials" integrity="sha256-oFeuE/P+XJMjkMS5pAPudQOMGJQ323nQt+DQ+9zbdAg= sha512-+EXjzt0I7g6BjvqqjkkboGyRlFSfIuyzY2SQ43HQKZBrHsjmRzEdjSHhiDzVs30nXL9H0tKw6WbMPc6RfzUumQ==" ></script>
<script src="https://example.com/thing"></script>
<script src="https://example.com/thing" integrity="thing"></script>

<!-- other -->
<link rel="icon" type="image/png" href="favicon.png" sizes="16x16" />
1 change: 1 addition & 0 deletions test/fixtures/output2/thing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('test');
1 change: 1 addition & 0 deletions test/fixtures/output2/unicode-chars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('I ♡ WebAppSec!');
52 changes: 45 additions & 7 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
var chai = require('chai');
var assert = chai.assert;
var fs = require('fs');
var broccoli = require('broccoli');
var plugin = require('../');
var lint = require('mocha-eslint');

function file(path) {
return fs.readFileSync(path, 'UTF-8').trim();;
}

describe('broccoli-sri-hash', function () {
var builder;

before(function() {
builder = new broccoli.Builder(plugin('test/fixtures/input', {
prefix: 'https://example.com/',
crossorigin: 'anonymous'
}));
});

it('rule outputs match', function () {
after(function() {
builder.cleanup();
});

var fileTmpContents = fs.readFileSync('tmp/output/test.html', {encoding: 'utf8'});
var fileContents = fs.readFileSync('test/fixtures/output/test.html', {encoding: 'utf8'});
it('rule outputs match (initial build)', function () {
return builder.build().then(function(output) {
var actual = file(output.directory + '/test.html');
var expected = file('test/fixtures/output/test.html');

assert.equal(fileTmpContents.trim(), fileContents.trim());
assert.equal(actual, expected);
});
});

it('Must lint', function () {
var fileTmpContents = fs.readFileSync('tmp/lint-out', {encoding: 'utf8'});
assert.notMatch(fileTmpContents, /[0-9]+\s+problems?\s\([0-9]+\serrors?,\s[0-9]+\swarnings?\)/)
it('rule outputs match (rebuild)', function () {
var pathToMutate = 'test/fixtures/input/other.css';
var originalContent = fs.readFileSync(pathToMutate);
return builder.build().then(function(output) {
// mutate input File
fs.writeFileSync('test/fixtures/input/other.css', '* { display: none; }');

return builder.build();
}).then(function(output) {
var actual = file(output.directory + '/test.html');
var expected = file('test/fixtures/output2/test.html');

assert.equal(actual, expected);
}).finally(function() {
fs.writeFileSync(pathToMutate, originalContent);
});
});

lint([
'index.js',
'tests/index.js'
]);
});

0 comments on commit 75c4b42

Please sign in to comment.