From 05e9d6f2a53cb73806e5e5124abc8105c92f5176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=27rysiek=27=20Wo=C5=BAniak?= Date: Wed, 1 Nov 2023 00:29:35 +0000 Subject: [PATCH] basic-integrity plugin now handles absolute paths as keys as well (ref. #70) --- plugins/basic-integrity/README.md | 6 ++- .../basic-integrity/__tests__/browser.test.js | 54 +++++++++++++++++++ plugins/basic-integrity/index.js | 27 ++++++++-- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/plugins/basic-integrity/README.md b/plugins/basic-integrity/README.md index 30a71b8..8ece076 100644 --- a/plugins/basic-integrity/README.md +++ b/plugins/basic-integrity/README.md @@ -17,8 +17,10 @@ The `basic-integrity` plugin supports the following configuration options: Array containing exactly one object which is in turn a configuration of a wrapped plugin. This plugin will be used to actually handle any requests. - `integrity` (default: empty) - An object mapping absolute URLs (e.g. "`https://example.com/img/test.png`") to integrity hashes (e.g. "`sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z`"). Supported integrity hash algorithms [as per SRI specification](https://w3c.github.io/webappsec-subresource-integrity/#terms): `sha256`, `sha384`, `sha512`. - The integrity string can contain multiple hashes, space-separated, [as per the standard](https://w3c.github.io/webappsec-subresource-integrity/#agility). + An object mapping URLs (e.g. "`https://example.com/img/test.png`") and absolute paths (e.g. "`/img/test.png`") to integrity hashes (e.g. "`sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z`"). Supported integrity hash algorithms [as per SRI specification](https://w3c.github.io/webappsec-subresource-integrity/#terms): `sha256`, `sha384`, `sha512`. + The integrity string can contain multiple hashes, space-separated, [as per the standard](https://w3c.github.io/webappsec-subresource-integrity/#agility). + When integrity data is specified twice for the same effective URL (once using full URL, once using absolute path), it is concatenated before passing the request on to the wrapped plugin. + Using absolute paths instead of URLs as keys is useful when hosting the same website content under multiple domain names, assuming the same paths are used across all domains. - `requireIntegrity` (default: `true`) Boolean value specifying if integrity data is required for a request to handled. That is: if a request is being handled for a URL that does not have integrity data associated with it, should the request be processed, or errored out? diff --git a/plugins/basic-integrity/__tests__/browser.test.js b/plugins/basic-integrity/__tests__/browser.test.js index ca1c03e..27c41c8 100644 --- a/plugins/basic-integrity/__tests__/browser.test.js +++ b/plugins/basic-integrity/__tests__/browser.test.js @@ -49,6 +49,11 @@ describe('browser: basic-integrity plugin', async () => { console.debug(component + ' :: ', ...items) } } + // mocking window.location + // https://developer.mozilla.org/en-US/docs/Web/API/Window/location + window.location = { + origin: "https://resilient.is" + } window.resolvingFetch = (url, init) => { return Promise.resolve( new Response( @@ -234,5 +239,54 @@ describe('browser: basic-integrity plugin', async () => { }) assertEquals(await response.json(), {test: "success"}) }); + + it("should set integrity data specified for absolute paths correctly on relevant requests", async () => { + init.integrity = { + "/test.json": "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z" + } + + const response = await LibResilientPluginConstructors + .get('basic-integrity')(LR, init) + .fetch('https://resilient.is/test.json'); + + assertSpyCalls(resolvingFetchSpy, 1) + assertSpyCall( + resolvingFetchSpy, + 0, + { + args: [ + 'https://resilient.is/test.json', + { + integrity: "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z" + } + ] + }) + assertEquals(await response.json(), {test: "success"}) + }); + + it("should concatenate integrity data specified for the same effective URL twice (by absolute path, and by URL)", async () => { + init.integrity = { + "/test.json": "sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z", + "https://resilient.is/test.json": "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ=" + } + + const response = await LibResilientPluginConstructors + .get('basic-integrity')(LR, init) + .fetch('https://resilient.is/test.json'); + + assertSpyCalls(resolvingFetchSpy, 1) + assertSpyCall( + resolvingFetchSpy, + 0, + { + args: [ + 'https://resilient.is/test.json', + { + integrity: "sha256-Aj9x0DWq9GUL1L8HibLCMa8YLKnV7IYAfpYurqrFwiQ= sha384-kn5dhxz4RpBmx7xC7Dmq2N43PclV9U/niyh+4Km7oz5W0FaWdz3Op+3K0Qxz8y3z" + } + ] + }) + assertEquals(await response.json(), {test: "success"}) + }); }) diff --git a/plugins/basic-integrity/index.js b/plugins/basic-integrity/index.js index 5168f4f..e10872c 100644 --- a/plugins/basic-integrity/index.js +++ b/plugins/basic-integrity/index.js @@ -24,9 +24,20 @@ name: "alt-fetch" }], // integrity data for each piece of content - // absolute URL -> integrity data (string) + // URL or absolute path -> integrity data (string) + // // integrity data can contain multiple integrity hashes, space-separated, as per: // https://w3c.github.io/webappsec-subresource-integrity/#agility + // + // integrity data specified for the same effective URL twice (using the whole URL, + // and then only the path) is concatenated + // + // for example, these two entries: + // "https://example.org/some/path/index.html": "sha384-..." + // "/some/path/index.html": "sha512-..." + // + // ...will result, if request URL is https://example.org/some/path/index.html and + // origin is https://example.org, in concatenated integrity data for that request. integrity: {}, // if an URL has no integrity data associated with it, should it be allowed or not? requireIntegrity: true @@ -57,11 +68,21 @@ } // do we have integrity data in config? - // TODO: how should we treat relative URLs? how does regular fetch() treat them + // + // the service worker would send the fetch our way only if the origin matched the URL + // and we only ever get full URLs anyway + + // trying a full URL (schema://example.com/...) if (url in config.integrity) { integrity += ' ' + config.integrity[url] } + // trying just the path, without schema and without the domain + let path = url.replace(self.location.origin, "") + if (path in config.integrity) { + integrity += ' ' + config.integrity[path] + } + // some cleanup integrity = integrity.trim() @@ -74,7 +95,7 @@ } else if (config.requireIntegrity) { // bail if integrity data is not available - throw new Error(`Integrity data required but not provided for: ${url}`) + throw new Error(`Integrity data required but not provided for: ${url} nor ${path}`) } // log