Skip to content

Commit 339bcec

Browse files
yoavweissmarcoscaceres
authored andcommitted
Add support for importmap integrity
https://bugs.webkit.org/show_bug.cgi?id=272884 Reviewed by Ryosuke Niwa. 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. Canonical link: https://commits.webkit.org/279096@main
1 parent 59ee460 commit 339bcec

22 files changed

Lines changed: 682 additions & 29 deletions

LayoutTests/TestExpectations

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6126,6 +6126,11 @@ imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/js
61266126
imported/w3c/web-platform-tests/import-maps/acquiring/modulepreload.html [ Skip ]
61276127
imported/w3c/web-platform-tests/import-maps/acquiring/modulepreload-link-header.html [ Skip ]
61286128

6129+
imported/w3c/web-platform-tests/import-maps/dynamic-integrity.html [ DumpJSConsoleLogInStdErr ]
6130+
imported/w3c/web-platform-tests/import-maps/static-integrity.html [ DumpJSConsoleLogInStdErr ]
6131+
imported/w3c/web-platform-tests/import-maps/no-referencing-script-integrity.html [ DumpJSConsoleLogInStdErr ]
6132+
imported/w3c/web-platform-tests/import-maps/nonimport-integrity.html [ DumpJSConsoleLogInStdErr ]
6133+
61296134
# These tests have been timing out since their import.
61306135
imported/w3c/web-platform-tests/import-maps/data-driven [ Skip ]
61316136

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
features:
2+
- name: import-maps
3+
files: "**"

LayoutTests/imported/w3c/web-platform-tests/import-maps/data-driven/resources/test-helper.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ function createTestIframe(importMap, importMapBaseURL) {
1919
iframe.src = 'data:text/html;base64,' + btoa(testHTML);
2020
} else {
2121
iframe.src = '/common/blank.html';
22-
iframe.onload = () => {
22+
iframe.addEventListener('load', () => {
2323
iframe.contentDocument.write(testHTML);
2424
iframe.contentDocument.close();
25-
};
25+
}, {once: true});
2626
}
2727
document.body.appendChild(iframe);
2828
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
PASS script was not loaded, as its resolved URL failed its integrity check
3+
PASS script was loaded, as its resolved URL had no integrity check, despite its specifier having one
4+
PASS script was loaded, as its integrity check passed
5+
PASS Script with no import definition was not loaded, as it failed its integrity check
6+
PASS Bare specifier script was not loaded, as it failed its integrity check
7+
PASS Bare specifier used for integrity loaded, as its definition should have used the URL
8+
PASS script was loaded, as its integrity check passed, despite having an extra invalid hash
9+
PASS script was loaded, as its integrity check passed, despite having an invalid suffix
10+
PASS script was loaded, as its integrity check passed given multiple hashes. This also makes sure that the larger hash is picked
11+
PASS script was loaded, as its integrity check was ignored, as it was defined using a URL that looks like a bare specifier
12+
PASS Script imported inside an event handler was loaded as its valid integrity check passed
13+
PASS Script imported inside an event handler was not loaded as its integrity check failed
14+
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<script src="/resources/testharness.js"></script>
4+
<script src="/resources/testharnessreport.js"></script>
5+
<script type="importmap">
6+
{
7+
"imports": {
8+
"./resources/log.js?pipe=sub&name=ResolvesToBadHash": "./resources/log.js?pipe=sub&name=BadHash",
9+
"./resources/log.js?pipe=sub&name=ResolvesToNoHash": "./resources/log.js?pipe=sub&name=NoHash",
10+
"./resources/log.js?pipe=sub&name=GoodHash": "./resources/log.js?pipe=sub&name=GoodHash",
11+
"bare": "./resources/log.js?pipe=sub&name=BareURL",
12+
"bare2": "./resources/log.js?pipe=sub&name=F"
13+
},
14+
"integrity": {
15+
"./resources/log.js?pipe=sub&name=BadHash": "sha384-foobar",
16+
"./resources/log.js?pipe=sub&name=ResolvesToNoHash": "sha384-foobar",
17+
"./resources/log.js?pipe=sub&name=GoodHash": "sha384-SwfgBqInhSlLziU454cYhGgwPpae+d3VHZcY+vjZIO/gxRGt2u3Jsfyvure/Ww0u",
18+
"./resources/log.js?pipe=sub&name=InvalidExtra": "sha384-WsKk8nzJFPhk/4pWR4LYoPhEu3xaAc6PdIm4vmqoZVWqEgMYmZgOg9XJKxgD1+8v foobar-rOJN8igD0+jW6lwNN3+InhXTgQztVHlq/HJ0riswXp8kMoiIDx5JpmCwuVem6Ll9q2LFNSu1xq23bsBMMQk1rg==",
19+
"./resources/log.js?pipe=sub&name=Suffix": "sha384-lbOWldbmji7sCHI/L8iVJ+elmFIMp41p+aYOLxqQfZMqtoFeHFVe/ASRA0IyZ1/9?foobar",
20+
"./resources/log.js?pipe=sub&name=Multiple": "sha384-foobar sha512-rOJN8igD0+jW6lwNN3+InhXTgQztVHlq/HJ0riswXp8kMoiIDx5JpmCwuVem6Ll9q2LFNSu1xq23bsBMMQk1rg==",
21+
"./resources/log.js?pipe=sub&name=BadHashWithNoImport": "sha384-foobar",
22+
"./resources/log.js?pipe=sub&name=BareURL": "sha384-foobar",
23+
"./resources/log.js?pipe=sub&name=EventHandlerPass": "sha384-d4yrBK8a55vlyYz2QEnlaU64PPpdKBkblD2KmfozI61mC1ij6RrZJaGCTsVxPuJ2",
24+
"./resources/log.js?pipe=sub&name=EventHandlerFail": "sha384-foobar",
25+
"bare2": "sha384-foobar",
26+
"resources/log.js?pipe=sub&name=Bare": "sha384-foobar"
27+
}
28+
}
29+
</script>
30+
<script>
31+
let log;
32+
const test_not_loaded = (url, description) => {
33+
promise_test(async t => {
34+
log = [];
35+
await promise_rejects_js(t, TypeError, import(url));
36+
assert_array_equals(log, []);
37+
}, description);
38+
};
39+
40+
const test_loaded = (url, log_expectation, description) => {
41+
promise_test(async t => {
42+
log = [];
43+
await import(url);
44+
assert_array_equals(log, log_expectation);
45+
}, description);
46+
};
47+
48+
test_not_loaded(
49+
"./resources/log.js?pipe=sub&name=ResolvesToBadHash",
50+
"script was not loaded, as its resolved URL failed its integrity check"
51+
);
52+
test_loaded(
53+
"./resources/log.js?pipe=sub&name=ResolvesToNoHash",
54+
["log:NoHash"],
55+
"script was loaded, as its resolved URL had no integrity check, despite its specifier having one"
56+
);
57+
test_loaded(
58+
"./resources/log.js?pipe=sub&name=GoodHash",
59+
["log:GoodHash"],
60+
"script was loaded, as its integrity check passed"
61+
);
62+
test_not_loaded(
63+
"./resources/log.js?pipe=sub&name=BadHashWithNoImport",
64+
"Script with no import definition was not loaded, as it failed its integrity check"
65+
);
66+
test_not_loaded(
67+
"bare",
68+
"Bare specifier script was not loaded, as it failed its integrity check"
69+
);
70+
test_loaded(
71+
"bare2",
72+
["log:F"],
73+
"Bare specifier used for integrity loaded, as its definition should have used the URL"
74+
);
75+
test_loaded(
76+
"./resources/log.js?pipe=sub&name=InvalidExtra",
77+
["log:InvalidExtra"],
78+
"script was loaded, as its integrity check passed, despite having an extra invalid hash"
79+
);
80+
test_loaded(
81+
"./resources/log.js?pipe=sub&name=Suffix",
82+
["log:Suffix"],
83+
"script was loaded, as its integrity check passed, despite having an invalid suffix"
84+
);
85+
test_loaded(
86+
"./resources/log.js?pipe=sub&name=Multiple",
87+
["log:Multiple"],
88+
"script was loaded, as its integrity check passed given multiple hashes. This also makes sure that the larger hash is picked"
89+
);
90+
test_loaded(
91+
"./resources/log.js?pipe=sub&name=Bare",
92+
["log:Bare"],
93+
"script was loaded, as its integrity check was ignored, as it was defined using a URL that looks like a bare specifier"
94+
);
95+
96+
promise_test(async () => {
97+
log = [];
98+
const img = new Image();
99+
const promise = new Promise((resolve, reject) => {
100+
img.onload = () => {
101+
import('./resources/log.js?pipe=sub&name=EventHandlerPass').then(resolve).catch(reject);
102+
};
103+
img.src = "/images/green.png?1";
104+
});
105+
106+
await promise;
107+
assert_equals(log.length, 1);
108+
assert_equals(log[0], "log:EventHandlerPass");
109+
}, "Script imported inside an event handler was loaded as its valid integrity check passed");
110+
111+
promise_test(async t => {
112+
log = [];
113+
const img = new Image();
114+
const promise = new Promise((resolve, reject) => {
115+
img.onload = () => {
116+
import('./resources/log.js?pipe=sub&name=EventHandlerFail').then(resolve).catch(reject);
117+
};
118+
img.src = "/images/green.png?2";
119+
});
120+
121+
await promise_rejects_js(t, TypeError, promise);
122+
}, "Script imported inside an event handler was not loaded as its integrity check failed");
123+
</script>
124+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
3+
PASS Script was not loaded as its integrity check failed
4+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
3+
PASS Script was loaded as its valid integrity check passed
4+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script>
7+
let log = [];
8+
</script>
9+
<script type="importmap">
10+
{
11+
"integrity": {
12+
"./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck": "sha384-5eRmXQSBE6H5ENdymdZxcyiIfJL1dxtH8p+hOelZY7Jzk+gt0gYyemrGY0cEaThF"
13+
}
14+
}
15+
</script>
16+
<script>
17+
let promiseResolve;
18+
let promiseReject;
19+
let promise = new Promise((resolve, reject) => {
20+
promiseResolve = resolve;
21+
promiseReject = reject;
22+
});
23+
</script>
24+
</head>
25+
<body>
26+
<!-- This is testing the part of
27+
https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule
28+
where step 6's condition is false and referencingScript remains null.
29+
Therefore, the onload event must be defined as an HTML attribute, outside of any script tag.
30+
-->
31+
<img src="/images/green.png?2"
32+
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck').then(promiseResolve).catch(promiseReject)">
33+
<script>
34+
promise_test(async () => {
35+
await promise;
36+
assert_equals(log.length, 1);
37+
assert_equals(log[0], "log:NoReferencingScriptValidCheck");
38+
}, "Script was loaded as its valid integrity check passed");
39+
</script>
40+
</body>
41+
</html>
42+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script>
7+
let log = [];
8+
</script>
9+
<script type="importmap">
10+
{
11+
"integrity": {
12+
"./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
13+
}
14+
}
15+
</script>
16+
<script>
17+
let promiseResolve;
18+
let promiseReject;
19+
let promise = new Promise((resolve, reject) => {
20+
promiseResolve = resolve;
21+
promiseReject = reject;
22+
});
23+
</script>
24+
</head>
25+
<body>
26+
<!-- This is testing the part of
27+
https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule
28+
where step 6's condition is false and referencingScript remains null.
29+
Therefore, the onload event must be defined as an HTML attribute, outside of any script tag.
30+
-->
31+
<img src="/images/green.png"
32+
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck').then(promiseResolve).catch(promiseReject)">
33+
<script type="module">
34+
promise_test(async t => {
35+
await promise_rejects_js(t, TypeError, promise);
36+
}, "Script was not loaded as its integrity check failed");
37+
</script>
38+
</body>
39+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
PASS Script was not loaded as its integrity check was not ignored
3+
PASS Script was loaded as its correct integrity attribute was not ignored
4+
PASS Script was loaded as its empty integrity attribute was not ignored
5+
PASS Script was not loaded as its bad integrity attribute was not overridden
6+
FAIL Modulepreload was not loaded as its integrity check was not ignored assert_unreached: Should have rejected: undefined Reached unreachable code
7+
PASS Modulepreload was loaded as its correct integrity attribute was not ignored
8+
PASS Modulepreload was loaded as its empty integrity attribute was not ignored
9+
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"
10+
PASS Classic script was loaded as its integrity check was ignored
11+
PASS Image was loaded as its integrity check was ignored
12+

0 commit comments

Comments
 (0)