| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- /**
- * 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 ReplaceSource = require('webpack-core/lib/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.emittedWarnings = {};
- }
- SubresourceIntegrityPlugin.prototype.emitMessage = function emitMessage(messages, message) {
- messages.push(new Error('webpack-subresource-integrity: ' + message));
- };
- SubresourceIntegrityPlugin.prototype.warnOnce = function warn(compilation, message) {
- if (!this.emittedWarnings[message]) {
- this.emittedWarnings[message] = true;
- this.emitMessage(compilation.warnings, message);
- }
- };
- SubresourceIntegrityPlugin.prototype.error = function error(compilation, message) {
- this.emitMessage(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;
- newAsset = new ReplaceSource(assets[chunkFile]);
- Array.from(hashByChunkId.entries()).forEach(function replaceMagicMarkers(idAndHash) {
- magicMarker = util.makePlaceholder(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(this.options.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;
- // This can happen with invalid Webpack configurations
- if (childChunk.files.length === 0) return;
- sourcePath = compilation.sriChunkAssets[childChunk.id];
- if (childChunk.files.indexOf(sourcePath) < 0) {
- self.warnOnce(
- compilation,
- 'Cannot determine asset for chunk ' + childChunk.id + ', computed="' + sourcePath +
- '", available=' + 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 = childChunk.files[0];
- }
- self.warnIfHotUpdate(compilation, assets[sourcePath].source());
- newAsset = self.replaceAsset(
- assets,
- hashByChunkId,
- sourcePath);
- hashByChunkId.set(childChunk.id, newAsset.integrity);
- });
- };
- SubresourceIntegrityPlugin.prototype.chunkAsset =
- function chunkAsset(compilation, chunk, asset) {
- // eslint-disable-next-line no-param-reassign
- compilation.sriChunkAssets[chunk.id] = asset;
- };
- /*
- * Calculate SRI values for each chunk and replace the magic
- * placeholders by the actual values.
- */
- SubresourceIntegrityPlugin.prototype.afterOptimizeAssets =
- function afterOptimizeAssets(compilation, assets) {
- var asset;
- var self = this;
- compilation.chunks.filter(util.isRuntimeChunk).forEach(function forEachChunk(chunk) {
- self.processChunk(chunk, compilation, assets);
- });
- Object.keys(assets).forEach(function loop(assetKey) {
- asset = assets[assetKey];
- if (!asset.integrity) {
- asset.integrity = util.computeIntegrity(self.options.hashFuncNames, asset.source());
- }
- });
- };
- SubresourceIntegrityPlugin.prototype.processTag =
- function processTag(compilation, tag) {
- var src = this.hwpAssetPath(util.getTagSrc(tag));
- var checksum = util.getIntegrityChecksumForAsset(compilation.assets, src);
- if (!checksum) {
- this.warnOnce(
- compilation,
- 'Cannot determine hash for asset \'' +
- src + '\', the resource will be unprotected.');
- return;
- }
- // Add integrity check sums
- /* eslint-disable no-param-reassign */
- tag.attributes.integrity = checksum;
- 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;
- ['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) {
- var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
- var chunkAsset = this.chunkAsset.bind(this, compilation);
- var alterAssetTags = this.alterAssetTags.bind(this, compilation);
- var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, 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) {
- compilation.hooks.afterOptimizeAssets.tap('SriPlugin', afterOptimizeAssets);
- compilation.hooks.chunkAsset.tap('SriPlugin', chunkAsset);
- compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', this.registerHwpHooks.bind(this, alterAssetTags, beforeHtmlGeneration));
- } else {
- compilation.plugin('after-optimize-assets', afterOptimizeAssets);
- compilation.plugin('chunk-asset', chunkAsset);
- compilation.plugin('html-webpack-plugin-alter-asset-tags', alterAssetTags);
- compilation.plugin('html-webpack-plugin-before-html-generation', beforeHtmlGeneration);
- }
- };
- SubresourceIntegrityPlugin.prototype.afterPlugins = function afterPlugins(compiler) {
- if (compiler.hooks) {
- compiler.hooks.thisCompilation.tap('SriPlugin', this.thisCompilation.bind(this, compiler));
- } 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;
|