index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /**
  2. * Copyright (c) 2015-present, Waysact Pty Ltd
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. var crypto = require('crypto');
  8. var path = require('path');
  9. var ReplaceSource = require('webpack-core/lib/ReplaceSource');
  10. var util = require('./util');
  11. var WebIntegrityJsonpMainTemplatePlugin = require('./jmtp');
  12. var HtmlWebpackPlugin;
  13. // https://www.w3.org/TR/2016/REC-SRI-20160623/#cryptographic-hash-functions
  14. var standardHashFuncNames = ['sha256', 'sha384', 'sha512'];
  15. try {
  16. // eslint-disable-next-line global-require
  17. HtmlWebpackPlugin = require('html-webpack-plugin');
  18. } catch (e) {
  19. if (!(e instanceof Error) || e.code !== 'MODULE_NOT_FOUND') {
  20. throw e;
  21. }
  22. }
  23. function SubresourceIntegrityPlugin(options) {
  24. var useOptions;
  25. if (options === null || typeof options === 'undefined') {
  26. useOptions = {};
  27. } else if (typeof options === 'object') {
  28. useOptions = options;
  29. } else {
  30. throw new Error('webpack-subresource-integrity: argument must be an object');
  31. }
  32. this.options = {
  33. enabled: true
  34. };
  35. Object.assign(this.options, useOptions);
  36. this.emittedWarnings = {};
  37. }
  38. SubresourceIntegrityPlugin.prototype.emitMessage = function emitMessage(messages, message) {
  39. messages.push(new Error('webpack-subresource-integrity: ' + message));
  40. };
  41. SubresourceIntegrityPlugin.prototype.warnOnce = function warn(compilation, message) {
  42. if (!this.emittedWarnings[message]) {
  43. this.emittedWarnings[message] = true;
  44. this.emitMessage(compilation.warnings, message);
  45. }
  46. };
  47. SubresourceIntegrityPlugin.prototype.error = function error(compilation, message) {
  48. this.emitMessage(compilation.errors, message);
  49. };
  50. SubresourceIntegrityPlugin.prototype.validateOptions = function validateOptions(compilation) {
  51. if (this.optionsValidated) {
  52. return;
  53. }
  54. this.optionsValidated = true;
  55. if (this.options.enabled && !compilation.compiler.options.output.crossOriginLoading) {
  56. this.warnOnce(
  57. compilation,
  58. 'SRI requires a cross-origin policy, defaulting to "anonymous". ' +
  59. 'Set webpack option output.crossOriginLoading to a value other than false ' +
  60. 'to make this warning go away. ' +
  61. 'See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-leakage'
  62. );
  63. }
  64. this.validateHashFuncNames(compilation);
  65. };
  66. SubresourceIntegrityPlugin.prototype.validateHashFuncNames =
  67. function validateHashFuncNames(compilation) {
  68. if (!Array.isArray(this.options.hashFuncNames)) {
  69. this.error(
  70. compilation,
  71. 'options.hashFuncNames must be an array of hash function names, ' +
  72. 'instead got \'' + this.options.hashFuncNames + '\'.');
  73. this.options.enabled = false;
  74. } else if (
  75. !this.options.hashFuncNames.every(this.validateHashFuncName.bind(this, compilation))
  76. ) {
  77. this.options.enabled = false;
  78. } else {
  79. this.warnStandardHashFunc(compilation);
  80. }
  81. };
  82. SubresourceIntegrityPlugin.prototype.warnStandardHashFunc =
  83. function warnStandardHashFunc(compilation) {
  84. var foundStandardHashFunc = false;
  85. var i;
  86. for (i = 0; i < this.options.hashFuncNames.length; i += 1) {
  87. if (standardHashFuncNames.indexOf(this.options.hashFuncNames[i]) >= 0) {
  88. foundStandardHashFunc = true;
  89. }
  90. }
  91. if (!foundStandardHashFunc) {
  92. this.warnOnce(
  93. compilation,
  94. 'It is recommended that at least one hash function is part of the set ' +
  95. 'for which support is mandated by the specification. ' +
  96. 'These are: ' + standardHashFuncNames.join(', ') + '. ' +
  97. 'See http://www.w3.org/TR/SRI/#cryptographic-hash-functions for more information.');
  98. }
  99. };
  100. SubresourceIntegrityPlugin.prototype.validateHashFuncName =
  101. function validateHashFuncName(compilation, hashFuncName) {
  102. if (typeof hashFuncName !== 'string' &&
  103. !(hashFuncName instanceof String)) {
  104. this.error(
  105. compilation,
  106. 'options.hashFuncNames must be an array of hash function names, ' +
  107. 'but contained ' + hashFuncName + '.');
  108. return false;
  109. }
  110. try {
  111. crypto.createHash(hashFuncName);
  112. } catch (error) {
  113. this.error(
  114. compilation,
  115. 'Cannot use hash function \'' + hashFuncName + '\': ' +
  116. error.message);
  117. return false;
  118. }
  119. return true;
  120. };
  121. /* Given a public URL path to an asset, as generated by
  122. * HtmlWebpackPlugin for use as a `<script src>` or `<link href`> URL
  123. * in `index.html`, return the path to the asset, suitable as a key
  124. * into `compilation.assets`.
  125. */
  126. SubresourceIntegrityPlugin.prototype.hwpAssetPath = function hwpAssetPath(src) {
  127. return path.relative(this.hwpPublicPath, src);
  128. };
  129. SubresourceIntegrityPlugin.prototype.warnIfHotUpdate = function warnIfHotUpdate(
  130. compilation, source
  131. ) {
  132. if (source.indexOf('webpackHotUpdate') >= 0) {
  133. this.warnOnce(
  134. compilation,
  135. 'webpack-subresource-integrity may interfere with hot reloading. ' +
  136. 'Consider disabling this plugin in development mode.'
  137. );
  138. }
  139. };
  140. SubresourceIntegrityPlugin.prototype.replaceAsset = function replaceAsset(
  141. assets,
  142. hashByChunkId,
  143. chunkFile
  144. ) {
  145. var oldSource = assets[chunkFile].source();
  146. var newAsset;
  147. var magicMarker;
  148. var magicMarkerPos;
  149. newAsset = new ReplaceSource(assets[chunkFile]);
  150. Array.from(hashByChunkId.entries()).forEach(function replaceMagicMarkers(idAndHash) {
  151. magicMarker = util.makePlaceholder(idAndHash[0]);
  152. magicMarkerPos = oldSource.indexOf(magicMarker);
  153. if (magicMarkerPos >= 0) {
  154. newAsset.replace(
  155. magicMarkerPos,
  156. (magicMarkerPos + magicMarker.length) - 1,
  157. idAndHash[1]);
  158. }
  159. });
  160. // eslint-disable-next-line no-param-reassign
  161. assets[chunkFile] = newAsset;
  162. newAsset.integrity = util.computeIntegrity(this.options.hashFuncNames, newAsset.source());
  163. return newAsset;
  164. };
  165. SubresourceIntegrityPlugin.prototype.processChunk = function processChunk(
  166. chunk, compilation, assets
  167. ) {
  168. var self = this;
  169. var newAsset;
  170. var hashByChunkId = new Map();
  171. Array.from(util.findChunks(chunk)).reverse().forEach(childChunk => {
  172. var sourcePath;
  173. // This can happen with invalid Webpack configurations
  174. if (childChunk.files.length === 0) return;
  175. sourcePath = compilation.sriChunkAssets[childChunk.id];
  176. if (childChunk.files.indexOf(sourcePath) < 0) {
  177. self.warnOnce(
  178. compilation,
  179. 'Cannot determine asset for chunk ' + childChunk.id + ', computed="' + sourcePath +
  180. '", available=' + childChunk.files[0] + '. Please report this full error message ' +
  181. 'along with your Webpack configuration at ' +
  182. 'https://github.com/waysact/webpack-subresource-integrity/issues/new'
  183. );
  184. sourcePath = childChunk.files[0];
  185. }
  186. self.warnIfHotUpdate(compilation, assets[sourcePath].source());
  187. newAsset = self.replaceAsset(
  188. assets,
  189. hashByChunkId,
  190. sourcePath);
  191. hashByChunkId.set(childChunk.id, newAsset.integrity);
  192. });
  193. };
  194. SubresourceIntegrityPlugin.prototype.chunkAsset =
  195. function chunkAsset(compilation, chunk, asset) {
  196. // eslint-disable-next-line no-param-reassign
  197. compilation.sriChunkAssets[chunk.id] = asset;
  198. };
  199. /*
  200. * Calculate SRI values for each chunk and replace the magic
  201. * placeholders by the actual values.
  202. */
  203. SubresourceIntegrityPlugin.prototype.afterOptimizeAssets =
  204. function afterOptimizeAssets(compilation, assets) {
  205. var asset;
  206. var self = this;
  207. compilation.chunks.filter(util.isRuntimeChunk).forEach(function forEachChunk(chunk) {
  208. self.processChunk(chunk, compilation, assets);
  209. });
  210. Object.keys(assets).forEach(function loop(assetKey) {
  211. asset = assets[assetKey];
  212. if (!asset.integrity) {
  213. asset.integrity = util.computeIntegrity(self.options.hashFuncNames, asset.source());
  214. }
  215. });
  216. };
  217. SubresourceIntegrityPlugin.prototype.processTag =
  218. function processTag(compilation, tag) {
  219. var src = this.hwpAssetPath(util.getTagSrc(tag));
  220. var checksum = util.getIntegrityChecksumForAsset(compilation.assets, src);
  221. if (!checksum) {
  222. this.warnOnce(
  223. compilation,
  224. 'Cannot determine hash for asset \'' +
  225. src + '\', the resource will be unprotected.');
  226. return;
  227. }
  228. // Add integrity check sums
  229. /* eslint-disable no-param-reassign */
  230. tag.attributes.integrity = checksum;
  231. tag.attributes.crossorigin = compilation.compiler.options.output.crossOriginLoading || 'anonymous';
  232. /* eslint-enable no-param-reassign */
  233. };
  234. SubresourceIntegrityPlugin.prototype.alterAssetTags =
  235. function alterAssetTags(compilation, pluginArgs, callback) {
  236. /* html-webpack-plugin has added an event so we can pre-process the html tags before they
  237. inject them. This does the work.
  238. */
  239. var processTag = this.processTag.bind(this, compilation);
  240. pluginArgs.head.filter(util.filterTag).forEach(processTag);
  241. pluginArgs.body.filter(util.filterTag).forEach(processTag);
  242. callback(null, pluginArgs);
  243. };
  244. /* Add jsIntegrity and cssIntegrity properties to pluginArgs, to
  245. * go along with js and css properties. These are later
  246. * accessible on `htmlWebpackPlugin.files`.
  247. */
  248. SubresourceIntegrityPlugin.prototype.beforeHtmlGeneration =
  249. function beforeHtmlGeneration(compilation, pluginArgs, callback) {
  250. var self = this;
  251. this.hwpPublicPath = pluginArgs.assets.publicPath;
  252. ['js', 'css'].forEach(function addIntegrity(fileType) {
  253. // eslint-disable-next-line no-param-reassign
  254. pluginArgs.assets[fileType + 'Integrity'] =
  255. pluginArgs.assets[fileType].map(function assetIntegrity(filePath) {
  256. return util.getIntegrityChecksumForAsset(compilation.assets, self.hwpAssetPath(filePath));
  257. });
  258. });
  259. callback(null, pluginArgs);
  260. };
  261. SubresourceIntegrityPlugin.prototype.registerJMTP = function registerJMTP(compilation) {
  262. var plugin = new WebIntegrityJsonpMainTemplatePlugin(this, compilation);
  263. if (plugin.apply) {
  264. plugin.apply(compilation.mainTemplate);
  265. } else {
  266. compilation.mainTemplate.apply(plugin);
  267. }
  268. };
  269. SubresourceIntegrityPlugin.prototype.registerHwpHooks =
  270. function registerHwpHooks(alterAssetTags, beforeHtmlGeneration, hwpCompilation) {
  271. var self = this;
  272. if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) {
  273. // HtmlWebpackPlugin >= 4
  274. HtmlWebpackPlugin.getHooks(hwpCompilation).beforeAssetTagGeneration.tapAsync(
  275. 'sri',
  276. this.beforeHtmlGeneration.bind(this, hwpCompilation)
  277. );
  278. HtmlWebpackPlugin.getHooks(hwpCompilation).alterAssetTags.tapAsync(
  279. 'sri',
  280. function cb(data, callback) {
  281. var processTag = self.processTag.bind(self, hwpCompilation);
  282. data.assetTags.scripts.filter(util.filterTag).forEach(processTag);
  283. data.assetTags.styles.filter(util.filterTag).forEach(processTag);
  284. callback(null, data);
  285. }
  286. );
  287. } else if (hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags &&
  288. hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
  289. // HtmlWebpackPlugin 3
  290. hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('SriPlugin', alterAssetTags);
  291. hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync('SriPlugin', beforeHtmlGeneration);
  292. }
  293. };
  294. SubresourceIntegrityPlugin.prototype.thisCompilation =
  295. function thisCompilation(compiler, compilation) {
  296. var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
  297. var chunkAsset = this.chunkAsset.bind(this, compilation);
  298. var alterAssetTags = this.alterAssetTags.bind(this, compilation);
  299. var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, compilation);
  300. this.validateOptions(compilation);
  301. if (!this.options.enabled) {
  302. return;
  303. }
  304. this.registerJMTP(compilation);
  305. // FIXME: refactor into separate per-compilation state
  306. // eslint-disable-next-line no-param-reassign
  307. compilation.sriChunkAssets = {};
  308. /*
  309. * html-webpack support:
  310. * Modify the asset tags before webpack injects them for anything with an integrity value.
  311. */
  312. if (compiler.hooks) {
  313. compilation.hooks.afterOptimizeAssets.tap('SriPlugin', afterOptimizeAssets);
  314. compilation.hooks.chunkAsset.tap('SriPlugin', chunkAsset);
  315. compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', this.registerHwpHooks.bind(this, alterAssetTags, beforeHtmlGeneration));
  316. } else {
  317. compilation.plugin('after-optimize-assets', afterOptimizeAssets);
  318. compilation.plugin('chunk-asset', chunkAsset);
  319. compilation.plugin('html-webpack-plugin-alter-asset-tags', alterAssetTags);
  320. compilation.plugin('html-webpack-plugin-before-html-generation', beforeHtmlGeneration);
  321. }
  322. };
  323. SubresourceIntegrityPlugin.prototype.afterPlugins = function afterPlugins(compiler) {
  324. if (compiler.hooks) {
  325. compiler.hooks.thisCompilation.tap('SriPlugin', this.thisCompilation.bind(this, compiler));
  326. } else {
  327. compiler.plugin('this-compilation', this.thisCompilation.bind(this, compiler));
  328. }
  329. };
  330. SubresourceIntegrityPlugin.prototype.apply = function apply(compiler) {
  331. if (compiler.hooks) {
  332. compiler.hooks.afterPlugins.tap('SriPlugin', this.afterPlugins.bind(this));
  333. } else {
  334. compiler.plugin('after-plugins', this.afterPlugins.bind(this));
  335. }
  336. };
  337. module.exports = SubresourceIntegrityPlugin;