Skip to content

Commit

Permalink
Add integrity attribute when generating hashes for linked scripts/styles
Browse files Browse the repository at this point in the history
  • Loading branch information
sersorrel committed May 7, 2021
1 parent 8bb9860 commit ccce97b
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 2 deletions.
96 changes: 96 additions & 0 deletions plugin.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,102 @@ describe('CspHtmlWebpackPlugin', () => {
});
});

describe('Adding integrity attribute', () => {
it('adds an integrity attribute to linked scripts and styles', (done) => {
const config = createWebpackConfig(
[
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'external-scripts-styles.html'
),
}),
new MiniCssExtractPlugin(),
new CspHtmlWebpackPlugin(),
],
undefined,
'index-styled.js',
{
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
}
);

webpackCompile(config, (_, html) => {
const scripts = html['index.html']('script[src]');
const styles = html['index.html']('link[rel="stylesheet"]');

scripts.each((i, script) => {
if (!script.attribs.src.startsWith('http')) {
expect(script.attribs.integrity).toEqual("sha256-IDmpTcnLo5Niek0rbHm9EEQtYiqYHApvDU+Rta9RdVU=");
} else {
expect(script.attribs.integrity).toBeUndefined();
}
})
styles.each((i, style) => {
if (!style.attribs.href.startsWith('http')) {
expect(style.attribs.integrity).toEqual("sha256-bFK7QzTObijstzDDaq2yN82QIYcoYx/EDD87NWCGiPw=");
} else {
expect(style.attribs.integrity).toBeUndefined();
}
});
done();
});
});

it('does not add an integrity attribute to inline scripts or styles', (done) => {
const config = createWebpackConfig(
[
new HtmlWebpackPlugin({
filename: path.join(WEBPACK_OUTPUT_DIR, 'index.html'),
template: path.join(
__dirname,
'test-utils',
'fixtures',
'with-script-and-style.html'
),
}),
new MiniCssExtractPlugin(),
new CspHtmlWebpackPlugin(),
],
undefined,
'index-styled.js',
{
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
}
);

webpackCompile(config, (_, html) => {
const scripts = html['index.html']('script:not([src])');
const styles = html['index.html']('style');

scripts.each((i, script) => {
expect(script.attribs.integrity).toBeUndefined();
})
styles.each((i, style) => {
expect(style.attribs.integrity).toBeUndefined();
});
done();
});
});
});

describe('Hash / Nonce enabled check', () => {
it("doesn't add hashes to any policy rule if that policy rule has been globally disabled", (done) => {
const config = createWebpackConfig([
Expand Down
63 changes: 61 additions & 2 deletions plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class CspHtmlWebpackPlugin {
// the additional options that this plugin allows
this.opts = Object.freeze({ ...defaultAdditionalOpts, ...additionalOpts });

// the calculated hashes for each file, indexed by filename
this.hashes = {};

// valid hashes from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Sources
if (!['sha256', 'sha384', 'sha512'].includes(this.opts.hashingMethod)) {
throw new Error(
Expand Down Expand Up @@ -262,6 +265,19 @@ class CspHtmlWebpackPlugin {
return `'${this.opts.hashingMethod}-${hashed}'`;
}

/**
* Gets the hash of a file that is a webpack asset, storing the hash in a cache.
* @param assets
* @param {string} filename
* @returns {string}
*/
hashFile(assets, filename) {
if (!Object.prototype.hasOwnProperty.call(this.hashes, filename)) {
this.hashes[filename] = this.hash(assets[filename].source());
}
return this.hashes[filename];
}

/**
* Calculates shas of the policy / selector we define
* @param {object} $ - the Cheerio instance
Expand Down Expand Up @@ -345,12 +361,12 @@ class CspHtmlWebpackPlugin {
.filter((filename) =>
includedScripts.includes(path.join(this.publicPath, filename))
)
.map((filename) => this.hash(compilation.assets[filename].source()));
.map((filename) => this.hashFile(compilation.assets, filename));
const linkedStyleShas = this.styleFilesToHash
.filter((filename) =>
includedStyles.includes(path.join(this.publicPath, filename))
)
.map((filename) => this.hash(compilation.assets[filename].source()));
.map((filename) => this.hashFile(compilation.assets, filename));

const builtPolicy = this.buildPolicy({
...this.policy,
Expand Down Expand Up @@ -395,6 +411,45 @@ class CspHtmlWebpackPlugin {
return compileCb(null, htmlPluginData);
}

/**
* Remove the public path from a URL, if present
* @param publicPath
* @param {string} path
* @returns {string}
*/
getFilename(publicPath, path) {
if (!publicPath || !path.startsWith(publicPath)) {
return path;
}
return path.substr(publicPath.length);
}

/**
* Add integrity attributes to asset tags
* @param compilation
* @param htmlPluginData
* @param compileCb
*/
addIntegrityAttributes(compilation, htmlPluginData, compileCb) {
if (this.hashEnabled['script-src'] !== false) {
htmlPluginData.assetTags.scripts.filter(tag => tag.attributes.src).forEach(tag => {
const filename = this.getFilename(compilation.options.output.publicPath, tag.attributes.src);
if (filename in compilation.assets) {
tag.attributes.integrity = this.hashFile(compilation.assets, filename).slice(1, -1);
}
});
}
if (this.hashEnabled['style-src'] !== false) {
htmlPluginData.assetTags.styles.filter(tag => tag.attributes.href).forEach(tag => {
const filename = this.getFilename(compilation.options.output.publicPath, tag.attributes.href);
if (filename in compilation.assets) {
tag.attributes.integrity = this.hashFile(compilation.assets, filename).slice(1, -1);
}
});
}
return compileCb(null, htmlPluginData);
}

/**
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
* @param compiler
Expand All @@ -413,6 +468,10 @@ class CspHtmlWebpackPlugin {
'CspHtmlWebpackPlugin',
this.getFilesToHash.bind(this)
);
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
'CspHtmlWebpackPlugin',
this.addIntegrityAttributes.bind(this, compilation)
)
});
}
}
Expand Down

0 comments on commit ccce97b

Please sign in to comment.