Skip to content

Commit

Permalink
Subresource Integrity support for ES modules, using importmaps
Browse files Browse the repository at this point in the history
SRI support for ES modules enables using them in documents that require
SRI for certain scripts for security reasons, as well as with the move
overarching require-sri-for CSP directive.

This CL implements whatwg/html#10269
based on https://github.com/guybedford/import-maps-extensions#integrity

Change-Id: Ida563334048d013ffc658f9783f9401930dd4689
Bug: 334251999
  • Loading branch information
Yoav Weiss authored and chromium-wpt-export-bot committed Apr 24, 2024
1 parent fe7d1b2 commit 2d3b37a
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 3 deletions.
86 changes: 86 additions & 0 deletions import-maps/dynamic-integrity.html
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/test-helper.js"></script>
<script type="importmap">
{
"imports": {
"./resources/log.js?pipe=sub&name=A": "./resources/log.js?pipe=sub&name=B",
"./resources/log.js?pipe=sub&name=C": "./resources/log.js?pipe=sub&name=D",
"./resources/log.js?pipe=sub&name=G": "./resources/log.js?pipe=sub&name=F",
"./resources/log.js?pipe=sub&name=X": "./resources/log.js?pipe=sub&name=X",
"bare": "./resources/log.js?pipe=sub&name=E",
"bare2": "./resources/log.js?pipe=sub&name=F"
},
"integrity": {
"./resources/log.js?pipe=sub&name=B": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=C": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=X": "sha384-mCon9M46vUfNK2Wb3yjvBmpBw/3hwB+wMYS8IzDBng+7//R5Qao35E1azo4gFVax",
"./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-lbOWldbmji7sCHI/L8iVJ+elmFIMp41p+aYOLxqQfZMqtoFeHFVe/ASRA0IyZ1/9 sha512-rOJN8igD0+jW6lwNN3+InhXTgQztVHlq/HJ0riswXp8kMoiIDx5JpmCwuVem6Ll9q2LFNSu1xq23bsBMMQk1rg==",
"./resources/log.js?pipe=sub&name=Y": "sha384-mCon9M46vUfNK2Wb3yjvBmpBw/3hwB+wMYS8IzDBng+7//R5Qao35E1azo4gFVax",
"./resources/log.js?pipe=sub&name=E": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+tr10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"bare2": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+tr10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"resources/log.js?pipe=sub&name=Bare": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+tr10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
}
}
</script>
<script type="module">
const test_not_loaded = (url, description) => {
promise_test(async t => {
log = [];
const promise = import(url);
await assert_promise_rejecting(promise, TypeError);
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=A",
'script was not loaded, as its resolved URL failed its integrity check');

test_loaded("./resources/log.js?pipe=sub&name=C", ["log:D"],
'script was loaded, as its resolved URL had no integrity check, despite' +
' its specifier having one');

test_loaded("./resources/log.js?pipe=sub&name=X", ["log:X"],
'script was loaded, as its integrity check passed');

test_not_loaded("./resources/log.js?pipe=sub&name=Y",
'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');
</script>

35 changes: 35 additions & 0 deletions import-maps/no-referencing-script-integrity-valid.html
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/test-helper.js"></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>
<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>

32 changes: 32 additions & 0 deletions import-maps/no-referencing-script-integrity.html
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/test-helper.js"></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>
<img src="/images/green.png"
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck').then(promiseResolve).catch(promiseReject)">
<script type="module">
promise_test(async () => {
await assert_promise_rejecting(promise, TypeError);
}, "Script was not loaded as its integrity check failed");
</script>
</body>
</html>
139 changes: 139 additions & 0 deletions import-maps/nonimport-integrity.html
@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/test-helper.js"></script>
<script type="importmap">
{
"integrity": {
"./resources/log.js?pipe=sub&name=ModuleNoIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModuleIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModuleEmptyIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModulePreloadNoIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModulePreloadIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModulePreloadEmptyIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=NonModule": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"/images/green.png": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
}
}
</script>
<script type="module">
const test_not_loaded = (url, description) => {
promise_test(async t => {
log = [];
const promise = import(url);
await assert_promise_rejecting(promise, {});
assert_array_equals(log, []);
}, description);
};

promise_test(async () => {
log = [];
const script = document.createElement("script");
script.type = "module";
script.src = "./resources/log.js?pipe=sub&name=ModuleNoIntegrity";
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
document.head.appendChild(script);
await assert_promise_rejecting(promise, {});
}, "Script was not loaded as its integrity check was not ignored");

promise_test(async () => {
log = [];
const script = document.createElement("script");
script.type = "module";
script.integrity = "sha384-QtZrhNFOSmHASHnBdmGg+zrVz5hjukCBakaqwT2pcG7w+QTa/niK16csP6kXAeXI";
script.src = "./resources/log.js?pipe=sub&name=ModuleIntegrity";
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
document.head.appendChild(script);
await promise;
assert_equals(log.length, 1);
assert_equals(log[0], "log:ModuleIntegrity");
}, "Script was loaded as its correct integrity attribute was not ignored");

promise_test(async () => {
log = [];
const script = document.createElement("script");
script.type = "module";
script.integrity = "";
script.src = "./resources/log.js?pipe=sub&name=ModuleEmptyIntegrity";
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
document.head.appendChild(script);
await promise;
assert_equals(log.length, 1);
assert_equals(log[0], "log:ModuleEmptyIntegrity");
}, "Script was loaded as its empty integrity attribute was not ignored");

promise_test(async () => {
const link = document.createElement("link");
link.rel = "modulepreload";
link.href = "./resources/log.js?pipe=sub&name=ModulePreloadNoIntegrity";
const promise = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
document.head.appendChild(link);
await assert_promise_rejecting(promise, {});
}, "Modulepreload was not loaded as its integrity check was not ignored");

promise_test(async () => {
const link = document.createElement("link");
link.rel = "modulepreload";
link.integrity = "sha384-iDG3WysExtjWvD9QwQrC7nGXRvO0jM+r7Z2cOLMDO2geMlEtmN9j9xfqHfzT45+9";
link.href = "./resources/log.js?pipe=sub&name=ModulePreloadIntegrity";
const promise = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
document.head.appendChild(link);
await promise;
}, "Modulepreload was loaded as its correct integrity attribute was not ignored");

promise_test(async () => {
const link = document.createElement("link");
link.rel = "modulepreload";
link.integrity = "";
link.href = "./resources/log.js?pipe=sub&name=ModulePreloadEmptyIntegrity";
const promise = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
document.head.appendChild(link);
await promise;
}, "Modulepreload was loaded as its empty integrity attribute was not ignored");

promise_test(async () => {
log = [];
const script = document.createElement("script");
script.src = "./resources/log.js?pipe=sub&name=NonModule";
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
document.head.appendChild(script);
await promise;
assert_equals(log.length, 1);
assert_equals(log[0], "log:NonModule");
}, "Non module Script was loaded as its integrity check was ignored");

promise_test(async () => {
const img = document.createElement("img");
const promise = new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
img.src = "/images/green.png";
document.head.appendChild(img);
await promise;
}, "Image was loaded as its integrity check was ignored");
</script>
</head>
9 changes: 9 additions & 0 deletions import-maps/resources/test-helper.js
Expand Up @@ -244,3 +244,12 @@ function doTests(importMapString, importMapBaseURL, tests) {
});
}, { explicit_done: true });
}

function assert_promise_rejecting(promise, expected) {
return promise.then(() => {
assert_unreached('Promise should have rejected');
}).catch(error => {
assert_equals(error.name, expected.name, 'Unexpected Error Name:');
});
};

68 changes: 68 additions & 0 deletions import-maps/static-integrity.html
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log = [];
</script>
<script type="importmap">
{
"imports": {
"./resources/log.js?pipe=sub&name=A": "./resources/log.js?pipe=sub&name=B",
"./resources/log.js?pipe=sub&name=C": "./resources/log.js?pipe=sub&name=D"
},
"integrity": {
"./resources/log.js?pipe=sub&name=B": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=D": "sha384-rxZqznFuOnvObm6JJKVmwzBXrsRG25IepqKDFHGhtitRu9YPjxPpRPMIu2hzvtxF",
"./resources/log.js?pipe=sub&name=X": "sha384-mCon9M46vUfNK2Wb3yjvBmpBw/3hwB+wMYS8IzDBng+7//R5Qao35E1azo4gFVzx",
"./resources/log.js?pipe=sub&name=Y": "sha384-u0yaFlBF39Au++qcn+MGL/Ml7UmuVfLymNJAz6Yyi4RqyUfWelcuAzVyE8Shs9xn",
"./resources/log.js?pipe=sub&name=Z": "sha384-u0yaFlBF39Au++qcn+MGL/Ml7UmuVfLymNJAz6Yyi4RqyUfWelcuAzVyE8Shs9xn"
}
}
</script>
<script type="module">
import './resources/log.js?pipe=sub&name=A';
</script>
<script type="module">
test(t => {
assert_array_equals(log, []);
}, 'Static script did not load as it failed its integrity check');
log = [];
</script>
<script type="module">
import './resources/log.js?pipe=sub&name=C';
</script>
<script type="module">
test(t => {
assert_array_equals(log, ["log:D"]);
}, 'Static script loaded as its integrity check passed');
log = [];
</script>
<script type="module">
import './resources/log.js?pipe=sub&name=X';
</script>
<script type="module">
test(t => {
assert_array_equals(log, []);
}, 'Static script did not load as it failed its integrity check, even' +
' without an import defined');
log = [];
</script>
<script type="module">
import './resources/log.js?pipe=sub&name=Y';
</script>
<script type="module">
test(t => {
assert_array_equals(log, ["log:Y"]);
}, 'Static script loaded as its integrity check passed without an import' +
' defined');
log = [];
</script>
<script type="module" src="./resources/log.js?pipe=sub&name=Z">;
</script>
<script type="module">
test(t => {
assert_array_equals(log, []);
}, 'HTML-based module script did not load as its integrity check failed.');
log = [];
</script>

0 comments on commit 2d3b37a

Please sign in to comment.