Skip to content

Commit 5eb9e1b

Browse files
Camille Lamychromium-wpt-export-bot
authored andcommitted
[DIP] Implement CORP checks on subresources
This CL implements the CORP checks on subresources for documents using Document-Isolation-Policy: isolate-and-require-corp and Document-Isolation-Policy: isolate-and-credentialless. In the later case, this check only applies to credentialled subresources. A follow-up CL (https://chromium-review.googlesource.com/c/chromium/src/+/5593879) will implement proper error messaging in DevTools. Bug: 333029144 Change-Id: I54812987b06187bbc69ee99f7fed88e5ce0b6713 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5537963 Reviewed-by: Tsuyoshi Horo <horo@chromium.org> Reviewed-by: Joe DeBlasio <jdeblasio@chromium.org> Reviewed-by: Mike West <mkwst@chromium.org> Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Reviewed-by: Mason Freed <masonf@chromium.org> Commit-Queue: Camille Lamy <clamy@chromium.org> Cr-Commit-Position: refs/heads/main@{#1309953}
1 parent c43a2c6 commit 5eb9e1b

11 files changed

Lines changed: 465 additions & 0 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This directory contains tests for `Document-Isolation-Policy`.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!doctype html>
2+
<meta name="timeout" content="long">
3+
<title>Document-Isolation-Policy: isolate-and-require-corp header and subresources</title>
4+
<script src=/resources/testharness.js></script>
5+
<script src=/resources/testharnessreport.js></script>
6+
<script src="/common/get-host-info.sub.js"></script>
7+
<script src="/common/utils.js"></script> <!-- Use token() to allow running tests in parallel -->
8+
<div id=log></div>
9+
<script>
10+
const HOST = get_host_info();
11+
const BASE = new URL("resources", location).pathname;
12+
13+
promise_test(async t => {
14+
const response = await fetch(get_host_info().HTTPS_REMOTE_ORIGIN+"/html/document-isolation-policy/resources/nothing-cross-origin-corp.js", {mode: "no-cors"});
15+
assert_equals(response.type, "opaque");
16+
}, `"isolate-and-require-corp" top-level: fetch() to CORP: cross-origin response should succeed`);
17+
18+
promise_test(async t => {
19+
return promise_rejects_js(t, TypeError, fetch(get_host_info().HTTPS_REMOTE_ORIGIN+"/html/document-isolation-policy/resources/nothing-no-corp.js", {mode: "no-cors"}));
20+
}, `"isolate-and-require-corp" top-level: fetch() to cross-origin response without CORP should fail`);
21+
22+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Document-Isolation-Policy: isolate-and-require-corp
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!doctype html>
2+
<meta charset="utf-8">
3+
<title>Fetch and create Blob</title>
4+
<script>
5+
async function responseToBlob(response) {
6+
let blob;
7+
try {
8+
blob = await response.blob();
9+
} catch (e) {
10+
return { error: `blob error: ${e.name}` };
11+
}
12+
13+
return { url: URL.createObjectURL(blob) };
14+
}
15+
16+
async function responseToData(response) {
17+
const mimeType = response.headers.get("content-type");
18+
19+
let text;
20+
try {
21+
text = await response.text();
22+
} catch(e) {
23+
return { error: `text error: ${e.name}` };
24+
}
25+
26+
return { url: `data:${mimeType},${encodeURIComponent(text)}` };
27+
}
28+
29+
async function responseToFilesystem(response) {
30+
if (!window.webkitRequestFileSystem) {
31+
return { error: "unimplemented" };
32+
}
33+
34+
let blob;
35+
try {
36+
blob = await response.blob();
37+
} catch (e) {
38+
return { error: `blob error: ${e.name}` };
39+
}
40+
41+
const fs = await new Promise(resolve => {
42+
window.webkitRequestFileSystem(window.TEMPORARY, 1024*1024, resolve);
43+
});
44+
45+
const file = await new Promise(resolve => {
46+
fs.root.getFile('fetch-and-create-url', { create: true }, resolve);
47+
});
48+
49+
const writer = await new Promise(resolve => file.createWriter(resolve));
50+
51+
try {
52+
await new Promise((resolve, reject) => {
53+
writer.onwriteend = resolve;
54+
writer.onerror = reject;
55+
writer.write(blob);
56+
});
57+
} catch (e) {
58+
return { error: `file write error: ${e.name}` };
59+
}
60+
61+
return { url: file.toURL() };
62+
}
63+
64+
async function responseToScheme(response, scheme) {
65+
switch (scheme) {
66+
case "blob":
67+
return responseToBlob(response);
68+
case "data":
69+
return responseToData(response);
70+
case "filesystem":
71+
return responseToFilesystem(response);
72+
default:
73+
return { error: `unknown scheme: ${scheme}` };
74+
}
75+
}
76+
77+
async function fetchToScheme(url, scheme) {
78+
let response;
79+
try {
80+
response = await fetch(url);
81+
} catch (e) {
82+
return { error: `fetch error: ${e.name}` };
83+
}
84+
85+
return responseToScheme(response, scheme);
86+
}
87+
88+
const params = new URL(window.location).searchParams;
89+
fetchToScheme(params.get("url"), params.get("scheme"))
90+
.then((message) => { parent.postMessage(message, "*"); });
91+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* Just an empty JS file */
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cross-Origin-Resource-Policy: cross-origin
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* Just an empty JS file */
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
body = b'''
2+
'use strict';
3+
4+
onconnect = (event) => {
5+
const port = event.ports[0];
6+
7+
port.onmessage = (event) => {
8+
fetch(event.data, { mode: 'no-cors' })
9+
.then(
10+
() => port.postMessage('success'),
11+
() => port.postMessage('failure')
12+
);
13+
};
14+
15+
port.postMessage('ready');
16+
};'''
17+
18+
def main(request, response):
19+
headers = [(b'Content-Type', b'text/javascript')]
20+
21+
for value in request.GET.get_list(b'value'):
22+
headers.append((b'Document-Isolation-Policy', value))
23+
24+
return (200, headers, body)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
onconnect = (event) => {
2+
const port = event.ports[0];
3+
port.onmessage = (event) => {
4+
eval(event.data);
5+
};
6+
port.postMessage('ready');
7+
};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Configures `url` such that the response carries a `COEP: ${value}` header.
2+
//
3+
// `url` must be a `URL` instance.
4+
function setDip(url, value) {
5+
url.searchParams
6+
.set("pipe", `header(document-isolation-policy,${value})`);
7+
}
8+
9+
// Resolves the given `relativeUrl` relative to the current window's location.
10+
//
11+
// `options` can contain the following keys:
12+
//
13+
// - `dip`: value passed to `setDip()`, if present.
14+
// - `host`: overrides the host of the returned URL.
15+
//
16+
// Returns a `URL` instance.
17+
function resolveUrl(relativeUrl, options) {
18+
const url = new URL(relativeUrl, window.location);
19+
20+
if (options !== undefined) {
21+
const { dip, host } = options;
22+
if (dip !== undefined) {
23+
setDip(url, dip);
24+
}
25+
if (host !== undefined) {
26+
url.host = host;
27+
}
28+
}
29+
30+
return url;
31+
}
32+
33+
// Adds an iframe loaded from `url` to the current document, waiting for it to
34+
// load before returning.
35+
//
36+
// The returned iframe is removed from the document at the end of the test `t`.
37+
async function withIframe(t, url) {
38+
const frame = document.createElement("iframe");
39+
frame.src = url;
40+
41+
t.add_cleanup(() => frame.remove());
42+
43+
const loadedPromise = new Promise(resolve => {
44+
frame.addEventListener('load', resolve, {once: true});
45+
});
46+
document.body.append(frame);
47+
await loadedPromise;
48+
49+
return frame;
50+
}
51+
52+
// Asynchronously waits for a single "message" event on the given `target`.
53+
function waitForMessage(target) {
54+
return new Promise(resolve => {
55+
target.addEventListener('message', resolve, {once: true});
56+
});
57+
}
58+
59+
// Fetches `url` from a document with DIP `creatorDip`, then serializes it
60+
// and returns a URL pointing to the fetched body with the given `scheme`.
61+
//
62+
// - `creatorDip` is passed to `setDip()`.
63+
// - `scheme` may be one of: "blob", "data" or "filesystem".
64+
//
65+
// The returned URL is valid until the end of the test `t`.
66+
async function createLocalUrl(t, { url, creatorDip, scheme }) {
67+
const frameUrl = resolveUrl("resources/fetch-and-create-url.html", {
68+
dip: creatorDip,
69+
});
70+
frameUrl.searchParams.set("url", url);
71+
frameUrl.searchParams.set("scheme", scheme);
72+
73+
const messagePromise = waitForMessage(window);
74+
const frame = await withIframe(t, frameUrl);
75+
76+
const evt = await messagePromise;
77+
const message = evt.data;
78+
assert_equals(message.error, undefined, "url creation error");
79+
80+
return message.url;
81+
}

0 commit comments

Comments
 (0)