You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
463 lines
16 KiB
463 lines
16 KiB
/**
|
|
* Copyright (c) 2015-present, Waysact Pty Ltd
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
var crypto = require('crypto');
|
|
var path = require('path');
|
|
var webpack = require('webpack');
|
|
// eslint-disable-next-line global-require
|
|
var ReplaceSource = (webpack.sources || require('webpack-sources')).ReplaceSource;
|
|
var util = require('./util');
|
|
var WebIntegrityJsonpMainTemplatePlugin = require('./jmtp');
|
|
var HtmlWebpackPlugin;
|
|
|
|
// https://www.w3.org/TR/2016/REC-SRI-20160623/#cryptographic-hash-functions
|
|
var standardHashFuncNames = ['sha256', 'sha384', 'sha512'];
|
|
|
|
try {
|
|
// eslint-disable-next-line global-require
|
|
HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
} catch (e) {
|
|
if (!(e instanceof Error) || e.code !== 'MODULE_NOT_FOUND') {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function SubresourceIntegrityPlugin(options) {
|
|
var useOptions;
|
|
if (options === null || typeof options === 'undefined') {
|
|
useOptions = {};
|
|
} else if (typeof options === 'object') {
|
|
useOptions = options;
|
|
} else {
|
|
throw new Error('webpack-subresource-integrity: argument must be an object');
|
|
}
|
|
|
|
this.options = {
|
|
enabled: true
|
|
};
|
|
|
|
Object.assign(this.options, useOptions);
|
|
|
|
this.emittedMessages = {};
|
|
|
|
this.assetIntegrity = new Map();
|
|
}
|
|
|
|
SubresourceIntegrityPlugin.prototype.emitMessage = function emitMessage(messages, message) {
|
|
messages.push(new Error('webpack-subresource-integrity: ' + message));
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.emitMessageOnce = function emitMessageOnce(messages, message) {
|
|
if (!this.emittedMessages[message]) {
|
|
this.emittedMessages[message] = true;
|
|
this.emitMessage(messages, message);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.warnOnce = function warn(compilation, message) {
|
|
this.emitMessageOnce(compilation.warnings, message);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.error = function error(compilation, message) {
|
|
this.emitMessage(compilation.errors, message);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.errorOnce = function error(compilation, message) {
|
|
this.emitMessageOnce(compilation.errors, message);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.validateOptions = function validateOptions(compilation) {
|
|
if (this.optionsValidated) {
|
|
return;
|
|
}
|
|
this.optionsValidated = true;
|
|
|
|
if (this.options.enabled && !compilation.compiler.options.output.crossOriginLoading) {
|
|
this.warnOnce(
|
|
compilation,
|
|
'SRI requires a cross-origin policy, defaulting to "anonymous". ' +
|
|
'Set webpack option output.crossOriginLoading to a value other than false ' +
|
|
'to make this warning go away. ' +
|
|
'See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-leakage'
|
|
);
|
|
}
|
|
this.validateHashFuncNames(compilation);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.validateHashFuncNames =
|
|
function validateHashFuncNames(compilation) {
|
|
if (!Array.isArray(this.options.hashFuncNames)) {
|
|
this.error(
|
|
compilation,
|
|
'options.hashFuncNames must be an array of hash function names, ' +
|
|
'instead got \'' + this.options.hashFuncNames + '\'.');
|
|
this.options.enabled = false;
|
|
} else if (
|
|
!this.options.hashFuncNames.every(this.validateHashFuncName.bind(this, compilation))
|
|
) {
|
|
this.options.enabled = false;
|
|
} else {
|
|
this.warnStandardHashFunc(compilation);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.warnStandardHashFunc =
|
|
function warnStandardHashFunc(compilation) {
|
|
var foundStandardHashFunc = false;
|
|
var i;
|
|
for (i = 0; i < this.options.hashFuncNames.length; i += 1) {
|
|
if (standardHashFuncNames.indexOf(this.options.hashFuncNames[i]) >= 0) {
|
|
foundStandardHashFunc = true;
|
|
}
|
|
}
|
|
if (!foundStandardHashFunc) {
|
|
this.warnOnce(
|
|
compilation,
|
|
'It is recommended that at least one hash function is part of the set ' +
|
|
'for which support is mandated by the specification. ' +
|
|
'These are: ' + standardHashFuncNames.join(', ') + '. ' +
|
|
'See http://www.w3.org/TR/SRI/#cryptographic-hash-functions for more information.');
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.validateHashFuncName =
|
|
function validateHashFuncName(compilation, hashFuncName) {
|
|
if (typeof hashFuncName !== 'string' &&
|
|
!(hashFuncName instanceof String)) {
|
|
this.error(
|
|
compilation,
|
|
'options.hashFuncNames must be an array of hash function names, ' +
|
|
'but contained ' + hashFuncName + '.');
|
|
return false;
|
|
}
|
|
try {
|
|
crypto.createHash(hashFuncName);
|
|
} catch (error) {
|
|
this.error(
|
|
compilation,
|
|
'Cannot use hash function \'' + hashFuncName + '\': ' +
|
|
error.message);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/* Given a public URL path to an asset, as generated by
|
|
* HtmlWebpackPlugin for use as a `<script src>` or `<link href`> URL
|
|
* in `index.html`, return the path to the asset, suitable as a key
|
|
* into `compilation.assets`.
|
|
*/
|
|
SubresourceIntegrityPlugin.prototype.hwpAssetPath = function hwpAssetPath(src) {
|
|
return path.relative(this.hwpPublicPath, src);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.warnIfHotUpdate = function warnIfHotUpdate(
|
|
compilation, source
|
|
) {
|
|
if (source.indexOf('webpackHotUpdate') >= 0) {
|
|
this.warnOnce(
|
|
compilation,
|
|
'webpack-subresource-integrity may interfere with hot reloading. ' +
|
|
'Consider disabling this plugin in development mode.'
|
|
);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.replaceAsset = function replaceAsset(
|
|
assets,
|
|
hashByChunkId,
|
|
chunkFile
|
|
) {
|
|
var oldSource = assets[chunkFile].source();
|
|
var newAsset;
|
|
var magicMarker;
|
|
var magicMarkerPos;
|
|
var hashFuncNames = this.options.hashFuncNames;
|
|
|
|
newAsset = new ReplaceSource(assets[chunkFile]);
|
|
|
|
Array.from(hashByChunkId.entries()).forEach(function replaceMagicMarkers(idAndHash) {
|
|
magicMarker = util.makePlaceholder(hashFuncNames, idAndHash[0]);
|
|
magicMarkerPos = oldSource.indexOf(magicMarker);
|
|
if (magicMarkerPos >= 0) {
|
|
newAsset.replace(
|
|
magicMarkerPos,
|
|
(magicMarkerPos + magicMarker.length) - 1,
|
|
idAndHash[1]);
|
|
}
|
|
});
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
assets[chunkFile] = newAsset;
|
|
|
|
newAsset.integrity = util.computeIntegrity(hashFuncNames, newAsset.source());
|
|
return newAsset;
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.processChunk = function processChunk(
|
|
chunk, compilation, assets
|
|
) {
|
|
var self = this;
|
|
var newAsset;
|
|
var hashByChunkId = new Map();
|
|
|
|
Array.from(util.findChunks(chunk)).reverse().forEach(childChunk => {
|
|
var sourcePath;
|
|
var files = Array.isArray(childChunk.files) ? new Set(childChunk.files) : childChunk.files;
|
|
|
|
// This can happen with invalid Webpack configurations
|
|
if (files.size === 0) return;
|
|
|
|
sourcePath = compilation.sriChunkAssets[childChunk.id];
|
|
|
|
if (!files.has(sourcePath)) {
|
|
self.warnOnce(
|
|
compilation,
|
|
'Cannot determine asset for chunk ' + childChunk.id + ', computed="' + sourcePath +
|
|
'", available=' + Array.from(childChunk.files)[0] + '. Please report this full error message ' +
|
|
'along with your Webpack configuration at ' +
|
|
'https://github.com/waysact/webpack-subresource-integrity/issues/new'
|
|
);
|
|
sourcePath = Array.from(files)[0];
|
|
}
|
|
|
|
self.warnIfHotUpdate(compilation, assets[sourcePath].source());
|
|
newAsset = self.replaceAsset(assets, hashByChunkId, sourcePath);
|
|
hashByChunkId.set(childChunk.id, newAsset.integrity);
|
|
this.assetIntegrity.set(sourcePath, newAsset.integrity);
|
|
});
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.chunkAsset =
|
|
function chunkAsset(compilation, chunk, asset) {
|
|
if (compilation.assets[asset]) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
compilation.sriChunkAssets[chunk.id] = asset;
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.statsFactory =
|
|
function stats(compilation, statsFactory) {
|
|
statsFactory.hooks.extract.for("asset").tap('SriPlugin', (object, asset) => {
|
|
if (this.assetIntegrity.has(asset.name)) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
object.integrity = String(this.assetIntegrity.get(asset.name));
|
|
}
|
|
});
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.addMissingIntegrityHashes =
|
|
function addMissingIntegrityHashes(assets) {
|
|
var self = this;
|
|
Object.keys(assets).forEach(function loop(assetKey) {
|
|
var asset = assets[assetKey];
|
|
if (!asset.integrity) {
|
|
asset.integrity = util.computeIntegrity(self.options.hashFuncNames, asset.source());
|
|
}
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Calculate SRI values for each chunk and replace the magic
|
|
* placeholders by the actual values.
|
|
*/
|
|
SubresourceIntegrityPlugin.prototype.afterOptimizeAssets =
|
|
function afterOptimizeAssets(compilation, assets) {
|
|
var self = this;
|
|
|
|
Array.from(compilation.chunks).filter(util.isRuntimeChunk).forEach(function forEachChunk(chunk) {
|
|
self.processChunk(chunk, compilation, assets);
|
|
});
|
|
|
|
this.addMissingIntegrityHashes(assets);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.processTag =
|
|
function processTag(compilation, tag) {
|
|
var src = this.hwpAssetPath(util.getTagSrc(tag));
|
|
/* eslint-disable no-param-reassign */
|
|
var integrity = util.getIntegrityChecksumForAsset(compilation.assets, src);
|
|
if (!Object.prototype.hasOwnProperty.call(tag.attributes, "integrity")) {
|
|
tag.attributes.integrity = integrity;
|
|
tag.attributes.crossorigin = compilation.compiler.options.output.crossOriginLoading || 'anonymous';
|
|
}
|
|
/* eslint-enable no-param-reassign */
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.alterAssetTags =
|
|
function alterAssetTags(compilation, pluginArgs, callback) {
|
|
/* html-webpack-plugin has added an event so we can pre-process the html tags before they
|
|
inject them. This does the work.
|
|
*/
|
|
var processTag = this.processTag.bind(this, compilation);
|
|
pluginArgs.head.filter(util.filterTag).forEach(processTag);
|
|
pluginArgs.body.filter(util.filterTag).forEach(processTag);
|
|
callback(null, pluginArgs);
|
|
};
|
|
|
|
|
|
/* Add jsIntegrity and cssIntegrity properties to pluginArgs, to
|
|
* go along with js and css properties. These are later
|
|
* accessible on `htmlWebpackPlugin.files`.
|
|
*/
|
|
SubresourceIntegrityPlugin.prototype.beforeHtmlGeneration =
|
|
function beforeHtmlGeneration(compilation, pluginArgs, callback) {
|
|
var self = this;
|
|
this.hwpPublicPath = pluginArgs.assets.publicPath;
|
|
this.addMissingIntegrityHashes(compilation.assets);
|
|
|
|
['js', 'css'].forEach(function addIntegrity(fileType) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
pluginArgs.assets[fileType + 'Integrity'] =
|
|
pluginArgs.assets[fileType].map(function assetIntegrity(filePath) {
|
|
return util.getIntegrityChecksumForAsset(compilation.assets, self.hwpAssetPath(filePath));
|
|
});
|
|
});
|
|
callback(null, pluginArgs);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.registerJMTP = function registerJMTP(compilation) {
|
|
var plugin = new WebIntegrityJsonpMainTemplatePlugin(this, compilation);
|
|
if (plugin.apply) {
|
|
plugin.apply(compilation.mainTemplate);
|
|
} else {
|
|
compilation.mainTemplate.apply(plugin);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.registerHwpHooks =
|
|
function registerHwpHooks(alterAssetTags, beforeHtmlGeneration, hwpCompilation) {
|
|
var self = this;
|
|
if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) {
|
|
// HtmlWebpackPlugin >= 4
|
|
HtmlWebpackPlugin.getHooks(hwpCompilation).beforeAssetTagGeneration.tapAsync(
|
|
'sri',
|
|
this.beforeHtmlGeneration.bind(this, hwpCompilation)
|
|
);
|
|
|
|
HtmlWebpackPlugin.getHooks(hwpCompilation).alterAssetTags.tapAsync(
|
|
'sri',
|
|
function cb(data, callback) {
|
|
var processTag = self.processTag.bind(self, hwpCompilation);
|
|
data.assetTags.scripts.filter(util.filterTag).forEach(processTag);
|
|
data.assetTags.styles.filter(util.filterTag).forEach(processTag);
|
|
callback(null, data);
|
|
}
|
|
);
|
|
} else if (hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags &&
|
|
hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
|
|
// HtmlWebpackPlugin 3
|
|
hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('SriPlugin', alterAssetTags);
|
|
hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync('SriPlugin', beforeHtmlGeneration);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.thisCompilation = function thisCompilation(
|
|
compiler,
|
|
compilation
|
|
) {
|
|
this.validateOptions(compilation);
|
|
|
|
if (!this.options.enabled) {
|
|
return;
|
|
}
|
|
|
|
this.registerJMTP(compilation);
|
|
|
|
// FIXME: refactor into separate per-compilation state
|
|
// eslint-disable-next-line no-param-reassign
|
|
compilation.sriChunkAssets = {};
|
|
|
|
/*
|
|
* html-webpack support:
|
|
* Modify the asset tags before webpack injects them for anything with an integrity value.
|
|
*/
|
|
if (compiler.hooks) {
|
|
this.setupHooks(compiler, compilation);
|
|
} else {
|
|
this.setupLegacyHooks(compiler, compilation);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.setupHooks = function setupHooks(
|
|
compiler,
|
|
compilation
|
|
) {
|
|
var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
|
|
var beforeChunkAssets = this.beforeChunkAssets.bind(this, compilation);
|
|
var alterAssetTags = this.alterAssetTags.bind(this, compilation);
|
|
var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, compilation);
|
|
|
|
if (compilation.hooks.processAssets) {
|
|
compilation.hooks.processAssets.tap(
|
|
{
|
|
name: 'SriPlugin',
|
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH
|
|
},
|
|
afterOptimizeAssets
|
|
);
|
|
} else {
|
|
compilation.hooks.afterOptimizeAssets.tap('SriPlugin', afterOptimizeAssets);
|
|
}
|
|
compilation.hooks.beforeChunkAssets.tap('SriPlugin', beforeChunkAssets);
|
|
compiler.hooks.compilation.tap(
|
|
'HtmlWebpackPluginHooks',
|
|
this.registerHwpHooks.bind(this, alterAssetTags, beforeHtmlGeneration)
|
|
);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.setupLegacyHooks = function setupLegacyHooks(
|
|
compiler,
|
|
compilation
|
|
) {
|
|
var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
|
|
var beforeChunkAssets = this.beforeChunkAssets.bind(this, compilation);
|
|
var alterAssetTags = this.alterAssetTags.bind(this, compilation);
|
|
var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, compilation);
|
|
|
|
compilation.plugin('after-optimize-assets', afterOptimizeAssets);
|
|
compilation.plugin('before-chunk-assets', beforeChunkAssets);
|
|
compilation.plugin('html-webpack-plugin-alter-asset-tags', alterAssetTags);
|
|
compilation.plugin(
|
|
'html-webpack-plugin-before-html-generation',
|
|
beforeHtmlGeneration
|
|
);
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.beforeChunkAssets = function afterPlugins(compilation) {
|
|
var chunkAsset = this.chunkAsset.bind(this, compilation);
|
|
if (compilation.hooks) {
|
|
compilation.hooks.chunkAsset.tap('SriPlugin', chunkAsset);
|
|
} else {
|
|
compilation.plugin('chunk-asset', chunkAsset);
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.afterPlugins = function afterPlugins(compiler) {
|
|
if (compiler.hooks) {
|
|
compiler.hooks.thisCompilation.tap('SriPlugin', this.thisCompilation.bind(this, compiler));
|
|
|
|
compiler.hooks.compilation.tap("DefaultStatsFactoryPlugin", compilation => {
|
|
if (compilation.hooks.statsFactory) {
|
|
compilation.hooks.statsFactory.tap('SriPlugin', this.statsFactory.bind(this, compilation));
|
|
}
|
|
});
|
|
|
|
} else {
|
|
compiler.plugin('this-compilation', this.thisCompilation.bind(this, compiler));
|
|
}
|
|
};
|
|
|
|
SubresourceIntegrityPlugin.prototype.apply = function apply(compiler) {
|
|
if (compiler.hooks) {
|
|
compiler.hooks.afterPlugins.tap('SriPlugin', this.afterPlugins.bind(this));
|
|
} else {
|
|
compiler.plugin('after-plugins', this.afterPlugins.bind(this));
|
|
}
|
|
};
|
|
|
|
module.exports = SubresourceIntegrityPlugin;
|