diff --git a/examples/lazy-e2e/index.js b/examples/lazy-e2e/index.js new file mode 100644 index 0000000..d99c34c --- /dev/null +++ b/examples/lazy-e2e/index.js @@ -0,0 +1 @@ +import("./nested1"); diff --git a/examples/lazy-e2e/nested1.js b/examples/lazy-e2e/nested1.js new file mode 100644 index 0000000..42c5706 --- /dev/null +++ b/examples/lazy-e2e/nested1.js @@ -0,0 +1 @@ +import("./nested2"); diff --git a/examples/lazy-e2e/nested2.js b/examples/lazy-e2e/nested2.js new file mode 100644 index 0000000..4f252c0 --- /dev/null +++ b/examples/lazy-e2e/nested2.js @@ -0,0 +1 @@ +console.log("ok"); diff --git a/examples/lazy-e2e/package.json b/examples/lazy-e2e/package.json new file mode 100644 index 0000000..45e5e8b --- /dev/null +++ b/examples/lazy-e2e/package.json @@ -0,0 +1,15 @@ +{ + "name": "lazy-e2e", + "description": "Basic end-to-end test with lazy-loaded hashes", + "version": "1.0.0", + "license": "MIT", + "private": true, + "devDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1", + "nyc": "*", + "webpack": "^5.44.0", + "webpack-cli": "4", + "webpack-subresource-integrity": "*", + "wsi-test-helper": "*" + } +} diff --git a/examples/lazy-e2e/webpack.config.js b/examples/lazy-e2e/webpack.config.js new file mode 100644 index 0000000..ebb34fa --- /dev/null +++ b/examples/lazy-e2e/webpack.config.js @@ -0,0 +1,21 @@ +const { SubresourceIntegrityPlugin } = require("webpack-subresource-integrity"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const { RunInPuppeteerPlugin } = require("wsi-test-helper"); + +module.exports = { + entry: { + index: "./index.js", + }, + output: { + crossOriginLoading: "anonymous", + }, + plugins: [ + new SubresourceIntegrityPlugin({ + hashFuncNames: ["sha256"], + enabled: true, + lazyHashes: true, + }), + new HtmlWebpackPlugin(), + new RunInPuppeteerPlugin(), + ], +}; diff --git a/examples/lazy-modified/README.md b/examples/lazy-modified/README.md new file mode 100644 index 0000000..64179ae --- /dev/null +++ b/examples/lazy-modified/README.md @@ -0,0 +1,4 @@ +# With lazy hashes and a modified dynamically loaded chunk #hwp + +Ensure that when a chunk is modified, it fails to load when hashes are +lazy-loaded. diff --git a/examples/lazy-modified/corrupt.js b/examples/lazy-modified/corrupt.js new file mode 100644 index 0000000..30bee6c --- /dev/null +++ b/examples/lazy-modified/corrupt.js @@ -0,0 +1 @@ +console.log("this should never load"); diff --git a/examples/lazy-modified/index.js b/examples/lazy-modified/index.js new file mode 100644 index 0000000..551ed35 --- /dev/null +++ b/examples/lazy-modified/index.js @@ -0,0 +1 @@ +import("./nested"); diff --git a/examples/lazy-modified/nested.js b/examples/lazy-modified/nested.js new file mode 100644 index 0000000..b55cb6b --- /dev/null +++ b/examples/lazy-modified/nested.js @@ -0,0 +1,7 @@ +import(/* webpackChunkName: "corrupt" */ "./corrupt") + .then(function error() { + console.log("error"); + }) + .catch(function ok() { + console.log("ok"); + }); diff --git a/examples/lazy-modified/package.json b/examples/lazy-modified/package.json new file mode 100644 index 0000000..32a158e --- /dev/null +++ b/examples/lazy-modified/package.json @@ -0,0 +1,15 @@ +{ + "name": "lazy-modified", + "description": "Ensure that when a chunk is modified, it fails to load when hashes are lazy-loaded.", + "version": "1.0.0", + "license": "MIT", + "private": true, + "devDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1", + "nyc": "*", + "webpack": "^5.44.0", + "webpack-cli": "4", + "webpack-subresource-integrity": "*", + "wsi-test-helper": "*" + } +} diff --git a/examples/lazy-modified/webpack.config.js b/examples/lazy-modified/webpack.config.js new file mode 100644 index 0000000..72ba574 --- /dev/null +++ b/examples/lazy-modified/webpack.config.js @@ -0,0 +1,43 @@ +const { SubresourceIntegrityPlugin } = require("webpack-subresource-integrity"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const { RunInPuppeteerPlugin } = require("wsi-test-helper"); +const { writeFileSync } = require("fs"); + +let gotError = false; + +module.exports = { + entry: { + index: "./index.js", + }, + output: { + crossOriginLoading: "anonymous", + }, + plugins: [ + new SubresourceIntegrityPlugin({ + hashFuncNames: ["sha256", "sha384"], + lazyHashes: true, + }), + new HtmlWebpackPlugin(), + new RunInPuppeteerPlugin({ + onStart: (stats) => { + console.log(stats.compilation.assets); + writeFileSync("dist/corrupt.js", 'console.log("corrupted");'); + }, + onConsoleError: (msg) => { + console.log(msg); + if ( + msg.match( + /Failed to find a valid digest in the 'integrity' attribute for resource/ + ) + ) { + gotError = true; + } + }, + onDone: () => { + if (!gotError) { + throw new Error("No error was raised"); + } + }, + }), + ], +}; diff --git a/webpack-subresource-integrity/index.ts b/webpack-subresource-integrity/index.ts index 43d5670..b7fe619 100644 --- a/webpack-subresource-integrity/index.ts +++ b/webpack-subresource-integrity/index.ts @@ -261,7 +261,9 @@ export class SubresourceIntegrityPlugin { "See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-leakage" ); } - return this.validateHashFuncNames(reporter); + return ( + this.validateHashFuncNames(reporter) && this.validateHashLoading(reporter) + ); }; /** @@ -291,6 +293,25 @@ export class SubresourceIntegrityPlugin { } }; + /** + * @internal + */ + private validateHashLoading = (reporter: Reporter): boolean => { + const supportedHashLoadingOptions = Object.freeze(["eager", "lazy"]); + if (supportedHashLoadingOptions.includes(this.options.hashLoading)) { + return true; + } + + const optionsStr = supportedHashLoadingOptions + .map((opt) => `'${opt}'`) + .join(", "); + + reporter.error( + `options.hashLoading must be one of ${optionsStr}, instead got '${this.options.hashLoading}'` + ); + return false; + }; + /** * @internal */ diff --git a/webpack-subresource-integrity/unit.test.ts b/webpack-subresource-integrity/unit.test.ts index 8579d66..7247cb5 100644 --- a/webpack-subresource-integrity/unit.test.ts +++ b/webpack-subresource-integrity/unit.test.ts @@ -8,6 +8,7 @@ import webpack, { Compiler, Compilation, Configuration, Chunk } from "webpack"; import HtmlWebpackPlugin from "html-webpack-plugin"; import { SubresourceIntegrityPlugin } from "./index.js"; +import type { SubresourceIntegrityPluginOptions } from "./index.js"; jest.unmock("html-webpack-plugin"); @@ -162,6 +163,26 @@ test("errors if hash function names contains unsupported digest", async () => { ); }); +test("errors if hashLoading option uses unknown value", async () => { + const plugin = new SubresourceIntegrityPlugin({ + hashLoading: + "invalid" as unknown as SubresourceIntegrityPluginOptions["hashLoading"], + }); + + const compilation = await runCompilation( + webpack({ + ...defaultOptions, + plugins: [plugin], + }) + ); + + expect(compilation.errors.length).toBe(1); + expect(compilation.warnings.length).toBe(0); + expect(compilation.errors[0].message).toMatch( + /options.hashLoading must be one of 'eager', 'lazy', instead got 'invalid'/ + ); +}); + test("uses default options", async () => { const plugin = new SubresourceIntegrityPlugin({ hashFuncNames: ["sha256"], diff --git a/yarn.lock b/yarn.lock index 31b1340..29ca9a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6815,6 +6815,19 @@ fsevents@^2.1.2: languageName: node linkType: hard +"lazy-e2e@workspace:examples/lazy-e2e": + version: 0.0.0-use.local + resolution: "lazy-e2e@workspace:examples/lazy-e2e" + dependencies: + html-webpack-plugin: ">= 5.0.0-beta.1" + nyc: "*" + webpack: ^5.44.0 + webpack-cli: 4 + webpack-subresource-integrity: "*" + wsi-test-helper: "*" + languageName: unknown + linkType: soft + "lazy-hashes-cycles@workspace:examples/lazy-hashes-cycles": version: 0.0.0-use.local resolution: "lazy-hashes-cycles@workspace:examples/lazy-hashes-cycles" @@ -6857,6 +6870,19 @@ fsevents@^2.1.2: languageName: unknown linkType: soft +"lazy-modified@workspace:examples/lazy-modified": + version: 0.0.0-use.local + resolution: "lazy-modified@workspace:examples/lazy-modified" + dependencies: + html-webpack-plugin: ">= 5.0.0-beta.1" + nyc: "*" + webpack: ^5.44.0 + webpack-cli: 4 + webpack-subresource-integrity: "*" + wsi-test-helper: "*" + languageName: unknown + linkType: soft + "lcov-parse@npm:^1.0.0": version: 1.0.0 resolution: "lcov-parse@npm:1.0.0"