From d37d95d0da7e3fdc4aad9c7e0e13c82886c26d07 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 27 Apr 2024 16:14:32 -0700 Subject: [PATCH 1/3] feat: support importmap integrity --- README.md | 19 +++++++++++++++++-- src/es-module-shims.js | 3 ++- src/resolve.js | 17 +++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0e0a84a9..651a9db0 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,9 @@ Using this polyfill we can write: "/": { "test-dep": "/test-dep.js" } + }, + "integrity": { + "/test.js": "sha386-..." } } @@ -277,6 +280,11 @@ Using this polyfill we can write: All modules are still loaded with the native browser module loader, but with their specifiers rewritten then executed as Blob URLs, so there is a relatively minimal overhead to using a polyfill approach like this. +#### Integrity + +The `"integrity"` field for import maps is supported when possible, throwing an error in es-module-shims when the integrity does not match +the expected value. + #### Multiple Import Maps Multiple import maps are not currently supported in any native implementation, Chromium support is currently being tracked in https://bugs.chromium.org/p/chromium/issues/detail?id=927119. @@ -636,17 +644,24 @@ This option can also be set to `true` to entirely disable the native passthrough ### Enforce Integrity -When enabled, `enforceIntegrity` will ensure that all modules loaded through ES Module Shims must have integrity defined either on a `` or on -a `` preload tag in shim mode. Modules without integrity will throw at fetch time. +When enabled, `enforceIntegrity` will ensure that all modules loaded through ES Module Shims must have integrity defined either on a ``, a `` preload tag in shim mode, or the `"integrity"` field in the import map. Modules without integrity will throw at fetch time. For example in the following, only the listed `app.js` and `dep.js` modules will be able to execute with the provided integrity: ```html + \ ``` diff --git a/src/es-module-shims.js b/src/es-module-shims.js index 9beb26c0..0c469e98 100755 --- a/src/es-module-shims.js +++ b/src/es-module-shims.js @@ -458,7 +458,8 @@ async function doFetch (url, fetchOpts, parent) { } async function fetchModule (url, fetchOpts, parent) { - const res = await doFetch(url, fetchOpts, parent); + const mapIntegrity = importMap.integrity[url]; + const res = await doFetch(url, mapIntegrity && !fetchOpts.integrity ? Object.assign({}, fetchOpts, { integrity: mapIntegrity }) : fetchOpts, parent); const r = res.url; const contentType = res.headers.get('content-type'); if (jsContentType.test(contentType)) diff --git a/src/resolve.js b/src/resolve.js index bfe8d91b..3278aaa4 100644 --- a/src/resolve.js +++ b/src/resolve.js @@ -1,6 +1,6 @@ import { mapOverrides, shimMode } from './env.js'; -export let importMap = { imports: Object.create(null), scopes: Object.create(null) }; +export let importMap = { imports: {}, scopes: {}, integrity: {} }; const backslashRegEx = /\\/g; @@ -100,7 +100,7 @@ export function resolveIfNotPlainOrUrl (relUrl, parentUrl) { } export function resolveAndComposeImportMap (json, baseUrl, parentMap) { - const outMap = { imports: Object.assign({}, parentMap.imports), scopes: Object.assign({}, parentMap.scopes) }; + const outMap = { imports: Object.assign({}, parentMap.imports), scopes: Object.assign({}, parentMap.scopes), integrity: Object.assign({}, parentMap.integrity) }; if (json.imports) resolveAndComposePackages(json.imports, outMap.imports, baseUrl, parentMap, null); @@ -111,6 +111,9 @@ export function resolveAndComposeImportMap (json, baseUrl, parentMap) { resolveAndComposePackages(json.scopes[s], outMap.scopes[resolvedScope] || (outMap.scopes[resolvedScope] = {}), baseUrl, parentMap); } + if (json.integrity) + resolveAndComposeIntegrity(json.integrity, outMap.integrity, baseUrl); + return outMap; } @@ -163,3 +166,13 @@ function resolveAndComposePackages (packages, outPackages, baseUrl, parentMap) { console.warn(`Mapping "${p}" -> "${packages[p]}" does not resolve`); } } + +function resolveAndComposeIntegrity (integrity, outIntegrity, baseUrl) { + for (let p in integrity) { + const resolvedLhs = resolveIfNotPlainOrUrl(p, baseUrl) || p; + if ((!shimMode || !mapOverrides) && outIntegrity[resolvedLhs] && (outIntegrity[resolvedLhs] !== integrity[resolvedLhs])) { + throw Error(`Rejected map integrity override "${resolvedLhs}" from ${outIntegrity[resolvedLhs]} to ${integrity[resolvedLhs]}.`); + } + outIntegrity[resolvedLhs] = integrity[p]; + } +} From 8e9ea2a646f19e901d0a997c61028e472c348967 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 27 Apr 2024 16:58:05 -0700 Subject: [PATCH 2/3] fixups and tests --- src/es-module-shims.js | 2 +- src/resolve.js | 2 -- test/shim.js | 11 +++++++---- test/test-shim.html | 9 ++++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/es-module-shims.js b/src/es-module-shims.js index 0c469e98..958f790d 100755 --- a/src/es-module-shims.js +++ b/src/es-module-shims.js @@ -146,7 +146,7 @@ async function loadAll (load, seen) { load.n = load.d.some(dep => dep.l.n); } -let importMap = { imports: {}, scopes: {} }; +let importMap = { imports: {}, scopes: {}, integrity: {} }; let baselinePassthrough; const initPromise = featureDetectionPromise.then(() => { diff --git a/src/resolve.js b/src/resolve.js index 3278aaa4..9a372376 100644 --- a/src/resolve.js +++ b/src/resolve.js @@ -1,7 +1,5 @@ import { mapOverrides, shimMode } from './env.js'; -export let importMap = { imports: {}, scopes: {}, integrity: {} }; - const backslashRegEx = /\\/g; export function asURL (url) { diff --git a/test/shim.js b/test/shim.js index 8aee6a68..7c41a523 100755 --- a/test/shim.js +++ b/test/shim.js @@ -293,7 +293,7 @@ suite('Get import map', () => { const sortEntriesByKey = (entries) => [...entries].sort(([key1], [key2]) => key1.localeCompare(key2)); const baseURL = document.location.href.replace(/\/test\/.*/, '/'); - assert.equal(JSON.stringify(Object.keys(importMap)), JSON.stringify(["imports", "scopes"])); + assert.equal(JSON.stringify(Object.keys(importMap)), JSON.stringify(["imports", "scopes", "integrity"])); assert.equal( JSON.stringify(sortEntriesByKey(Object.entries(importMap.imports))), JSON.stringify(sortEntriesByKey(Object.entries({ @@ -376,6 +376,9 @@ suite('Errors', function () { "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/dev.index.js", "scheduler/tracing": "https://ga.jspm.io/npm:scheduler@0.20.2/dev.tracing.js" } + }, + "integrity": { + "//ga.jspm.io/npm:scheduler@0.20.2/dev.index.js": "sha384-qF0Jy83btjdPADN4QLKKmk/aUUyJnDqT+kYomKiUQk4nWrBsHVkM67Pua+8nHYUt" } }); const [React, ReactDOM] = await Promise.all([ @@ -394,7 +397,7 @@ suite('Errors', function () { }); const lodash = await importShim("lodash"); assert.ok(lodash); - }) + }); test('Dynamic import map shim with override attempt', async function () { const listeningForError = new Promise((resolve, reject) => { @@ -415,7 +418,7 @@ suite('Errors', function () { removeImportMap(); assert(error.message.match(new RegExp(String.raw`Rejected map override \"global1\" from http://[^/]+/test/fixtures/es-modules/global1.js to data:text/javascript,throw new Error\('Shim should not allow dynamic import map to override existing entries'\);\.`))); - }) + }); test('Dynamic import map shim with override to the same mapping is allowed', async function () { const expectingNoError = new Promise((resolve, reject) => { @@ -436,7 +439,7 @@ suite('Errors', function () { await expectingNoError; removeImportMap(); - }) + }); function insertDynamicImportMap(importMap) { const script = Object.assign(document.createElement('script'), { diff --git a/test/test-shim.html b/test/test-shim.html index 9d2e5736..11fbb306 100644 --- a/test/test-shim.html +++ b/test/test-shim.html @@ -17,6 +17,9 @@ "/test/fixtures/es-modules/import-relative-path.js": { "./fixtures/es-modules/relative-path": "./fixtures/es-modules/es6-dep.js" } + }, + "integrity": { + "./fixtures/es-modules/es6-dep.js": "sha384-WBlJ+EO4b/8SuvC2RFiCt9z42esd35mcYuzHGIlqiltBvS6X11jqp06aksdZWflh" } } @@ -66,14 +69,14 @@