Skip to content

Commit

Permalink
Add support for importmap integrity
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=272884

Reviewed by NOBODY (OOPS!).

Imported ES modules can't currently have integrity checks, which means
they can't be used in sites where integrity checks are a necessity, for
security and privacy reasons.
This implements such support, by adding an "integrity" section to import
maps.

See whatwg/html#10269

* LayoutTests/TestExpectations: Ignored console logs to avoid flakiness
* LayoutTests/imported/w3c/web-platform-tests/import-maps/WEB_FEATURES.yml: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/data-driven/resources/test-helper.js:
(createTestIframe): Updated through import.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/dynamic-integrity-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/dynamic-integrity.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity-valid-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity-valid.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/nonimport-integrity-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/nonimport-integrity.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/static-integrity-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/static-integrity.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/import-maps/w3c-import.log: Imports.
* LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-resources.https-expected.txt: Updated.
* LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-resources.https.html: Updated to cover Request.integrity.
* LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html: Updated to cover Request.integrity.
* LayoutTests/platform/glib/imported/w3c/web-platform-tests/service-workers/service-worker/fetch-request-resources.https-expected.txt: Updated.
* Source/JavaScriptCore/runtime/ImportMap.cpp:
(JSC::ImportMap::resolveImportMatch): Typos and spec link.
(JSC::parseURLLikeModuleSpecifier): Typos and spec link.
(JSC::ImportMap::resolve const): Typos and spec link.
(JSC::normalizeSpecifierKey): Typos and spec link.
(JSC::sortAndNormalizeSpecifierMap): Typos and spec link.
(JSC::ImportMap::registerImportMap): Add parsing for the integrity
section.
(JSC::ImportMap::getIntegrity const): Getter for integrity based on URL.
* Source/JavaScriptCore/runtime/ImportMap.h:
* Source/WebCore/bindings/js/ScriptModuleLoader.cpp:
(WebCore::ScriptModuleLoader::importModule): Add integrity to outgoing
requests.
(WebCore::ScriptModuleLoader::notifyFinished): Enforce integrity from
the importmap on responses, even if integrity wasn't present in the
request. Needed for static imports triggered by JSCore.
* Source/WebCore/dom/ScriptElement.cpp:
(WebCore::ScriptElement::requestModuleScript): Add integrity to outgoing
requests for top-level modules, if they don't already have an integrity
attribute.
  • Loading branch information
yoavweiss committed May 21, 2024
1 parent adec935 commit b224e85
Show file tree
Hide file tree
Showing 22 changed files with 682 additions and 29 deletions.
5 changes: 5 additions & 0 deletions LayoutTests/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -6092,6 +6092,11 @@ imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/js
imported/w3c/web-platform-tests/import-maps/acquiring/modulepreload.html [ Skip ]
imported/w3c/web-platform-tests/import-maps/acquiring/modulepreload-link-header.html [ Skip ]

imported/w3c/web-platform-tests/import-maps/dynamic-integrity.html [ DumpJSConsoleLogInStdErr ]
imported/w3c/web-platform-tests/import-maps/static-integrity.html [ DumpJSConsoleLogInStdErr ]
imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity.html [ DumpJSConsoleLogInStdErr ]
imported/w3c/web-platform-tests/import-maps/nonimport-integrity.html [ DumpJSConsoleLogInStdErr ]

# These tests have been timing out since their import.
imported/w3c/web-platform-tests/import-maps/data-driven [ Skip ]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
features:
- name: import-maps
files: "**"
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ function createTestIframe(importMap, importMapBaseURL) {
iframe.src = 'data:text/html;base64,' + btoa(testHTML);
} else {
iframe.src = '/common/blank.html';
iframe.onload = () => {
iframe.addEventListener('load', () => {
iframe.contentDocument.write(testHTML);
iframe.contentDocument.close();
};
}, {once: true});
}
document.body.appendChild(iframe);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

PASS script was not loaded, as its resolved URL failed its integrity check
PASS script was loaded, as its resolved URL had no integrity check, despite its specifier having one
PASS script was loaded, as its integrity check passed
PASS Script with no import definition was not loaded, as it failed its integrity check
PASS Bare specifier script was not loaded, as it failed its integrity check
PASS Bare specifier used for integrity loaded, as its definition should have used the URL
PASS script was loaded, as its integrity check passed, despite having an extra invalid hash
PASS script was loaded, as its integrity check passed, despite having an invalid suffix
PASS script was loaded, as its integrity check passed given multiple hashes. This also makes sure that the larger hash is picked
PASS script was loaded, as its integrity check was ignored, as it was defined using a URL that looks like a bare specifier
PASS Script imported inside an event handler was loaded as its valid integrity check passed
PASS Script imported inside an event handler was not loaded as its integrity check failed

Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script type="importmap">
{
"imports": {
"./resources/log.js?pipe=sub&name=ResolvesToBadHash": "./resources/log.js?pipe=sub&name=BadHash",
"./resources/log.js?pipe=sub&name=ResolvesToNoHash": "./resources/log.js?pipe=sub&name=NoHash",
"./resources/log.js?pipe=sub&name=GoodHash": "./resources/log.js?pipe=sub&name=GoodHash",
"bare": "./resources/log.js?pipe=sub&name=BareURL",
"bare2": "./resources/log.js?pipe=sub&name=F"
},
"integrity": {
"./resources/log.js?pipe=sub&name=BadHash": "sha384-foobar",
"./resources/log.js?pipe=sub&name=ResolvesToNoHash": "sha384-foobar",
"./resources/log.js?pipe=sub&name=GoodHash": "sha384-SwfgBqInhSlLziU454cYhGgwPpae+d3VHZcY+vjZIO/gxRGt2u3Jsfyvure/Ww0u",
"./resources/log.js?pipe=sub&name=InvalidExtra": "sha384-WsKk8nzJFPhk/4pWR4LYoPhEu3xaAc6PdIm4vmqoZVWqEgMYmZgOg9XJKxgD1+8v foobar-rOJN8igD0+jW6lwNN3+InhXTgQztVHlq/HJ0riswXp8kMoiIDx5JpmCwuVem6Ll9q2LFNSu1xq23bsBMMQk1rg==",
"./resources/log.js?pipe=sub&name=Suffix": "sha384-lbOWldbmji7sCHI/L8iVJ+elmFIMp41p+aYOLxqQfZMqtoFeHFVe/ASRA0IyZ1/9?foobar",
"./resources/log.js?pipe=sub&name=Multiple": "sha384-foobar sha512-rOJN8igD0+jW6lwNN3+InhXTgQztVHlq/HJ0riswXp8kMoiIDx5JpmCwuVem6Ll9q2LFNSu1xq23bsBMMQk1rg==",
"./resources/log.js?pipe=sub&name=BadHashWithNoImport": "sha384-foobar",
"./resources/log.js?pipe=sub&name=BareURL": "sha384-foobar",
"./resources/log.js?pipe=sub&name=EventHandlerPass": "sha384-d4yrBK8a55vlyYz2QEnlaU64PPpdKBkblD2KmfozI61mC1ij6RrZJaGCTsVxPuJ2",
"./resources/log.js?pipe=sub&name=EventHandlerFail": "sha384-foobar",
"bare2": "sha384-foobar",
"resources/log.js?pipe=sub&name=Bare": "sha384-foobar"
}
}
</script>
<script>
let log;
const test_not_loaded = (url, description) => {
promise_test(async t => {
log = [];
await promise_rejects_js(t, TypeError, import(url));
assert_array_equals(log, []);
}, description);
};

const test_loaded = (url, log_expectation, description) => {
promise_test(async t => {
log = [];
await import(url);
assert_array_equals(log, log_expectation);
}, description);
};

test_not_loaded(
"./resources/log.js?pipe=sub&name=ResolvesToBadHash",
"script was not loaded, as its resolved URL failed its integrity check"
);
test_loaded(
"./resources/log.js?pipe=sub&name=ResolvesToNoHash",
["log:NoHash"],
"script was loaded, as its resolved URL had no integrity check, despite its specifier having one"
);
test_loaded(
"./resources/log.js?pipe=sub&name=GoodHash",
["log:GoodHash"],
"script was loaded, as its integrity check passed"
);
test_not_loaded(
"./resources/log.js?pipe=sub&name=BadHashWithNoImport",
"Script with no import definition was not loaded, as it failed its integrity check"
);
test_not_loaded(
"bare",
"Bare specifier script was not loaded, as it failed its integrity check"
);
test_loaded(
"bare2",
["log:F"],
"Bare specifier used for integrity loaded, as its definition should have used the URL"
);
test_loaded(
"./resources/log.js?pipe=sub&name=InvalidExtra",
["log:InvalidExtra"],
"script was loaded, as its integrity check passed, despite having an extra invalid hash"
);
test_loaded(
"./resources/log.js?pipe=sub&name=Suffix",
["log:Suffix"],
"script was loaded, as its integrity check passed, despite having an invalid suffix"
);
test_loaded(
"./resources/log.js?pipe=sub&name=Multiple",
["log:Multiple"],
"script was loaded, as its integrity check passed given multiple hashes. This also makes sure that the larger hash is picked"
);
test_loaded(
"./resources/log.js?pipe=sub&name=Bare",
["log:Bare"],
"script was loaded, as its integrity check was ignored, as it was defined using a URL that looks like a bare specifier"
);

promise_test(async () => {
log = [];
const img = new Image();
const promise = new Promise((resolve, reject) => {
img.onload = () => {
import('./resources/log.js?pipe=sub&name=EventHandlerPass').then(resolve).catch(reject);
};
img.src = "/images/green.png?1";
});

await promise;
assert_equals(log.length, 1);
assert_equals(log[0], "log:EventHandlerPass");
}, "Script imported inside an event handler was loaded as its valid integrity check passed");

promise_test(async t => {
log = [];
const img = new Image();
const promise = new Promise((resolve, reject) => {
img.onload = () => {
import('./resources/log.js?pipe=sub&name=EventHandlerFail').then(resolve).catch(reject);
};
img.src = "/images/green.png?2";
});

await promise_rejects_js(t, TypeError, promise);
}, "Script imported inside an event handler was not loaded as its integrity check failed");
</script>

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


PASS Script was not loaded as its integrity check failed

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


PASS Script was loaded as its valid integrity check passed

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log = [];
</script>
<script type="importmap">
{
"integrity": {
"./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck": "sha384-5eRmXQSBE6H5ENdymdZxcyiIfJL1dxtH8p+hOelZY7Jzk+gt0gYyemrGY0cEaThF"
}
}
</script>
<script>
let promiseResolve;
let promiseReject;
let promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
</script>
</head>
<body>
<!-- This is testing the part of
https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule
where step 6's condition is false and referencingScript remains null.
Therefore, the onload event must be defined as an HTML attribute, outside of any script tag.
-->
<img src="/images/green.png?2"
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck').then(promiseResolve).catch(promiseReject)">
<script>
promise_test(async () => {
await promise;
assert_equals(log.length, 1);
assert_equals(log[0], "log:NoReferencingScriptValidCheck");
}, "Script was loaded as its valid integrity check passed");
</script>
</body>
</html>

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log = [];
</script>
<script type="importmap">
{
"integrity": {
"./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
}
}
</script>
<script>
let promiseResolve;
let promiseReject;
let promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
</script>
</head>
<body>
<!-- This is testing the part of
https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule
where step 6's condition is false and referencingScript remains null.
Therefore, the onload event must be defined as an HTML attribute, outside of any script tag.
-->
<img src="/images/green.png"
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck').then(promiseResolve).catch(promiseReject)">
<script type="module">
promise_test(async t => {
await promise_rejects_js(t, TypeError, promise);
}, "Script was not loaded as its integrity check failed");
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

PASS Script was not loaded as its integrity check was not ignored
PASS Script was loaded as its correct integrity attribute was not ignored
PASS Script was loaded as its empty integrity attribute was not ignored
PASS Script was not loaded as its bad integrity attribute was not overridden
FAIL Modulepreload was not loaded as its integrity check was not ignored assert_unreached: Should have rejected: undefined Reached unreachable code
PASS Modulepreload was loaded as its correct integrity attribute was not ignored
PASS Modulepreload was loaded as its empty integrity attribute was not ignored
FAIL Modulepreload was not loaded as its bad integrity attribute was not ignored promise_test: Unhandled rejection with value: object "Error: It shouldn't have loaded"
PASS Classic script was loaded as its integrity check was ignored
PASS Image was loaded as its integrity check was ignored

0 comments on commit b224e85

Please sign in to comment.