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 16, 2024
1 parent 7ebd92c commit 1de24e9
Show file tree
Hide file tree
Showing 5 changed files with 407 additions and 0 deletions.
128 changes: 128 additions & 0 deletions import-maps/dynamic-integrity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log;
</script>
<script type="importmap" onload="t.assert_unreached('onload')" onerror="t.done()">
{
"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",
"./resources/log.js?pipe=sub&name=Suffix": "sha384-lbOWldbmji7sCHI/L8iVJ+elmFIMp41p+aYOLxqQfZMqtoFeHFVe/ASRA0IyZ1/9",
"./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"
}
}
</script>
<script type="module">
promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=A")
.then(() => assert_unreached())
.catch(() => assert_equals(log.length, 0))
},
'script was not loaded, as its resolved URL failed its integrity check');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=C")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:D");
})
.catch((e) => assert_unreached(e))
},
'script was loaded, as its resolved URL had no integrity check, despite' +
' its specifier having one');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=X")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:X");
})
.catch((e) => assert_unreached(e))
},
'script was loaded, as its integrity check passed');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=Y")
.then(() => assert_unreached())
.catch(() => assert_equals(log.length, 0))
},
'Script with no import definition was not loaded, as it failed its' +
' integrity check');

promise_test(() => {
log = [];
return import("bare")
.then(() => assert_unreached())
.catch(() => assert_equals(log.length, 0))
},
'Bare specifier script was not loaded, as it failed its integrity check');

promise_test(() => {
log = [];
return import("bare2")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:F");
})
.catch((e) => assert_unreached(e))
},
'Bare specifier used for integrity loaded, as its definition should have' +
' used the URL');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=InvalidExtra")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:InvalidExtra");
})
.catch((e) => assert_unreached(e))
},
'script was loaded, as its integrity check passed, despite having an extra' +
' invalid hash');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=Suffix")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:Suffix");
})
.catch((e) => assert_unreached(e))
},
'script was loaded, as its integrity check passed, despite having an' +
' invalid suffix');

promise_test(() => {
log = [];
return import("./resources/log.js?pipe=sub&name=Multiple")
.then(() => {
assert_equals(log.length, 1);
assert_equals(log[0], "log:Multiple");
})
.catch((e) => assert_unreached(e))
},
'script was loaded, as its integrity check passed given multiple hashes.' +
' This also makes sure that the larger hash is picked.');
</script>

42 changes: 42 additions & 0 deletions import-maps/no-referencing-script-integrity-valid.html
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" onload="t.assert_unreached('onload')" onerror="t.done()">
{
"integrity": {
"./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck": "sha384-5eRmXQSBE6H5ENdymdZxcyiIfJL1dxtH8p+hOelZY7Jzk+gt0gYyemrGY0cEaThF"
}
}
</script>
</head>
<body>
<script>
log = [];
let promiseResolve;
let promiseReject;
let promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
</script>
<img src="/images/green.png?2"
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptValidCheck').then(promiseResolve).catch(promiseReject)">
<script>
promise_test(async () => {
try {
await promise;
} catch {
assert_unreached();
}
assert_equals(log.length, 1);
assert_equals(log[0], "log:NoReferencingScriptValidCheck");
}, "Script was loaded as its valid integrity check passed");
</script>
</body>
</html>

40 changes: 40 additions & 0 deletions import-maps/no-referencing-script-integrity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log;
</script>
<script type="importmap" onload="t.assert_unreached('onload')" onerror="t.done()">
{
"integrity": {
"./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
}
}
</script>
</head>
<body>
<script>
log = [];
let promiseResolve;
let promiseReject;
let promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
</script>
<img src="/images/green.png"
onload="import('./resources/log.js?pipe=sub&name=NoReferencingScriptInvalidCheck').then(promiseResolve).catch(promiseReject)">
<script type="module">
promise_test(async () => {
try {
await promise;
assert_unreached();
} catch {
assert_equals(log.length, 0);
}
}, "Script was not loaded as its integrity check failed");
</script>
</body>
</html>
127 changes: 127 additions & 0 deletions import-maps/nonimport-integrity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
let log;
</script>
<script type="importmap" onload="t.assert_unreached('onload')" onerror="t.done()">
{
"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=ModulePreloadNoIntegrity": "sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7",
"./resources/log.js?pipe=sub&name=ModulePreloadIntegrity": "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">
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);
try {
await promise;
} catch {
assert_unreached();
}
assert_equals(log.length, 1);
assert_equals(log[0], "log:ModuleNoIntegrity");
}, "Script was loaded as its integrity check was 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);
try {
await promise;
} catch {
assert_unreached();
}
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 () => {
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);
try {
await promise;
} catch {
assert_unreached();
}
}, "Modulepreload was loaded as its integrity check was 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);
try {
await promise;
} catch {
assert_unreached();
}
}, "Modulepreload was loaded as its correct 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);
try {
await promise;
} catch {
assert_unreached();
}
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);
try {
await promise;
} catch {
assert_unreached();
}
}, "Image was loaded as its integrity check was ignored");
</script>
</head>
Loading

0 comments on commit 1de24e9

Please sign in to comment.