diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/html/cross-origin-embedder-policy | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/html/cross-origin-embedder-policy')
153 files changed, 7188 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml b/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml new file mode 100644 index 0000000000..dc7010880b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/META.yml @@ -0,0 +1,9 @@ +spec: https://html.spec.whatwg.org/multipage/origin.html#coep +suggested_reviewers: + - mikewest + - jugglinmike + - arturjanc + - lweichselbaum + - hemeryar + - ParisMeuleman + - valenting diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/README.md b/testing/web-platform/tests/html/cross-origin-embedder-policy/README.md new file mode 100644 index 0000000000..16179eb013 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/README.md @@ -0,0 +1 @@ +See `../cross-origin-opener-policy/README.md`. diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html new file mode 100644 index 0000000000..2dc73c7561 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html @@ -0,0 +1,59 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/script-factory.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script> + const origins = get_host_info(); + + promise_test(async t => { + const popup = window.open(); + t.add_cleanup(() => { popup.close(); }); + + let data_from_popup = () => new Promise(resolve => + window.addEventListener("message", (({ data }) => resolve(data)))); + + let check_result = (data, text) => { + assert_equals(data.origin, origin); + assert_true(data.sameOriginNoCORPSuccess, + text + ": Same-origin without CORP did not succeed"); + assert_true(data.crossOriginNoCORPFailure, + text + ": Cross-origin without CORP did not fail"); + }; + + // Check if COEP is inherited by the popup. + let script = popup.document.createElement('script'); + script.innerHTML = + `${createScript(window.origin, origins.HTTPS_REMOTE_ORIGIN, "opener")}`; + popup.document.body.appendChild(script); + check_result(await data_from_popup(), "Initial empty document"); + + // Navigate the popup away. + popup.location = origins.HTTPS_REMOTE_ORIGIN + + "/html/cross-origin-embedder-policy/resources/postmessage-ready.html"; + assert_equals(await new Promise(resolve => + window.addEventListener("message", msg => resolve(msg.data))), + "ready"); + + // Navigate the popup to about:blank and wait for it. + popup.location = "about:blank"; + await t.step_wait( + condition = () => { + try { + return popup.location.href === "about:blank"; + } catch {} + return false; + }, + description = "Wait for the popup to navigate.", + timeout=3000, + interval=50); + + // Check again if COEP is inherited. + script = popup.document.createElement('script'); + script.innerHTML = + `${createScript(window.origin, origins.HTTPS_REMOTE_ORIGIN, "opener")}`; + popup.document.body.appendChild(script); + check_result(await data_from_popup(), "Non-initial about:blank document"); + }, `Cross-Origin-Embedder-Policy is inherited by about:blank popup.`); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/about-blank-popup.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html new file mode 100644 index 0000000000..ce72f247ef --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html @@ -0,0 +1,44 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<div id=log></div> +<script> +const origins = get_host_info(); +[ + { + "origin": origins.HTTPS_ORIGIN, + "crossOrigin": origins.HTTPS_REMOTE_ORIGIN + }, + { + "origin": origins.HTTPS_REMOTE_ORIGIN, + "crossOrigin": origins.HTTPS_NOTSAMESITE_ORIGIN + }, + { + "origin": origins.HTTPS_NOTSAMESITE_ORIGIN, + "crossOrigin": origins.HTTPS_ORIGIN + } +].forEach(({ origin, crossOrigin }) => { + ["subframe", "navigate", "popup"].forEach(variant => { + async_test(t => { + const id = token(); + const frame = document.createElement("iframe"); + t.add_cleanup(() => { frame.remove(); }); + const path = new URL("resources/blob-url-factory.html", window.location).pathname; + frame.src = `${origin}${path}?id=${id}&variant=${variant}&crossOrigin=${crossOrigin}`; + window.addEventListener("message", t.step_func(({ data }) => { + if (data.id !== id) { + return; + } + assert_equals(data.origin, origin); + assert_true(data.sameOriginNoCORPSuccess, "Same-origin without CORP did not succeed"); + assert_true(data.crossOriginNoCORPFailure, "Cross-origin without CORP did not fail"); + t.done(); + })); + document.body.append(frame); + }, `Cross-Origin-Embedder-Policy and blob: URL from ${origin} in subframe via ${variant}`); + }); +}); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/blob.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/block-local-documents-inheriting-none.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/block-local-documents-inheriting-none.https.html new file mode 100644 index 0000000000..cf5176606e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/block-local-documents-inheriting-none.https.html @@ -0,0 +1,112 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<div id=log></div> + +<script> +const script = ` + <script> + top.postMessage({event: "loaded", type: location.protocol}, "*"); + <\/script>`; + +const test_cases = [ + {name: "data", url: encodeURI(`data:text/html,${script}`)}, + {name: "blob", url: URL.createObjectURL(new Blob([script], { type: "text/html" }))}, + {name: "about", url: "about:blank"}, + ]; + +const observeReports = async (frame) => { + const reports = []; + + const observer = new frame.contentWindow.ReportingObserver( + rs => reports.push(...rs.map(r => r.toJSON())) + ); + observer.observe(); + + // Wait for reports. Use a timeout to catch both expected and unexpected + // reports. + await new Promise(resolve => step_timeout(resolve, 3000)); + return reports; +}; + +promise_test(async t => { + const this_window_token = token(); + + // Expect the nested frame to not load, since they inherit COEP: none from the + // top frame, which is incompatible with first_frame's COEP: require-corp. + const received_events = []; + addEventListener("message", event => { + if(event.data.event == "loaded") + received_events.push(`Nested ${event.data.type} loaded!`); + }); + + // Create an iframe with COEP: require-corp + const first_iframe = document.createElement("iframe"); + t.add_cleanup( () => first_iframe.remove() ); + first_iframe.src = "/common/blank.html?pipe=header(cross-origin-embedder-policy,require-corp)"; + let iframe_load_promise = new Promise( resolve => first_iframe.addEventListener("load", resolve) ); + + document.body.append(first_iframe); + await iframe_load_promise; + + const reportPromise = observeReports(first_iframe); + // 1. Create nested frames. + // They initially navigate to blank.html and have COEP: require-corp set. + // This initial navigation is required because it uses the parent frame as the + // initiator. That is first_iframe is the initiator, while we want top to be + // the initiator for this test, which will be done in step 4 with a second + // navigation from that blank.html document to the local scheme one. + const nested_frames = {}; + const nested_frames_promises = []; + test_cases.forEach(test => { + nested_frame = document.createElement("iframe"); + nested_frame.src = "/common/blank.html?pipe=header(cross-origin-embedder-policy,require-corp)"; + t.add_cleanup( () => nested_frame.remove() ); + nested_frames_promises.push(new Promise( resolve => nested_frame.addEventListener("load", resolve) ) ); + first_iframe.contentDocument.body.append(nested_frame); + nested_frames[test.name] = nested_frame; + }); + + // 2. Wait for the loads of all iframes to complete. + await Promise.all(nested_frames_promises); + + // 3. Navigate a dummy frame. This is required because some browsers (Chrome) + // might consider the first navigation in 4. as a redirect otherwise. + const dummy_Frame = document.createElement("iframe"); + t.add_cleanup( () => dummy_Frame.remove() ); + dummy_Frame.src = "/common/blank.html"; + iframe_load_promise = new Promise( resolve => dummy_Frame.addEventListener("load", resolve) ); + document.body.append(dummy_Frame); + await iframe_load_promise; + + // 4. Navigate nested frames to a local scheme document. + // COEP should be inherited from the initiator or blobURL's creator (top in both + // cases), this results in COEP being none and the documents not being allowed + // to load under the COEP: require-corp iframe (first_iframe). + test_cases.forEach(test => { + // Top navigates nested_frame_[test.name] to a test.url + const frame = nested_frames[test.name]; + // Use frame.contentDocument.location to ensure the initiator is this (top) + // frame. frame.src is not used here as this makes the parent of the nested + // frame (first_iframe) the initiator. + frame.contentDocument.location = test.url; + }); + + // 5. Wait and validate reports. + const reports = await reportPromise; + assert_equals(reports.length, test_cases.length); + test_cases.forEach(test => { + assert_true(reports.some( report => { + return report.type == 'coep' && + report.body.type == 'navigation' && + report.body.blockedURL == test.url; + }), `No report matched for test "${test.name}"`); + }); + // Also verify that no message was sent by the nested frames and stored in + // received_events. + assert_equals(received_events.length, 0); +}, "Prevent local scheme documents from loading within a COEP: require-corp iframe if they inherit COEP: none"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-dedicated-worker.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-dedicated-worker.https.html new file mode 100644 index 0000000000..f4b2599141 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-dedicated-worker.https.html @@ -0,0 +1,51 @@ +<!doctype html> +<html> +<head> + <title> + Check COEP report are send for CacheStorage requests in DedicatedWorker + </title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/common/utils.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"> + </script> + <script src="./resources/cache-storage-reporting.js"> </script> +</head> +<script> + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep)); + const worker = new Worker(worker_url); + const mc = new MessageChannel(); + worker.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "enforce"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP support on DedicatedWorker.") + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep_report_only)); + const worker = new Worker(worker_url); + const mc = new MessageChannel(); + worker.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "reporting"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP-Report-Only support on DedicatedWorker.") + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-document.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-document.https.html new file mode 100644 index 0000000000..b998ba7926 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-document.https.html @@ -0,0 +1,58 @@ +<!doctype html> +<html> +<head> + <title> + Check COEP report are send for CacheStorage requests in Document. + </title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/common/utils.js"></script> + </script> + <script src="./resources/cache-storage-reporting.js"></script> +</head> +<script> + +async function waitReports(frame) { + return await new Promise((resolve) => { + const observer = new frame.contentWindow.ReportingObserver((reports) => { + observer.disconnect(); + resolve(reports.map(r => r.toJSON())); + }); + observer.observe(); + + frame.contentWindow.eval(eval_script); + }); +} + +promise_test(async (t) => { + const iframe_url = local(encode(iframe_path + header_coep)); + const iframe = await makeIframe(t, iframe_url); + const reports = await waitReports(iframe); + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "enforce"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, iframe_url); +}, "COEP support on document.") + +promise_test(async (t) => { + const iframe_url = local(encode(iframe_path + header_coep_report_only)); + const iframe = await makeIframe(t, iframe_url); + const reports = await waitReports(iframe); + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "reporting"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, iframe_url); +}, "COEP-Report-Only support on document.") + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-service-worker.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-service-worker.https.html new file mode 100644 index 0000000000..96a328b2cc --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-service-worker.https.html @@ -0,0 +1,64 @@ +<!doctype html> +<html> +<head> + <title> + Check COEP report are send for CacheStorage requests in ServiceWorker. + </title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/common/utils.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"> + </script> + <script src="./resources/cache-storage-reporting.js"></script> +</head> +<script> + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep)); + // As we don't want the service worker to control any page, generate a + // one-time scope. + const SCOPE = new URL(`resources/${token()}.html`, location).pathname; + const reg = + await service_worker_unregister_and_register(t, worker_url, SCOPE); + add_completion_callback(() => reg.unregister()); + const worker = reg.installing || reg.waiting || reg.active; + const mc = new MessageChannel(); + worker.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_not_equals(reports, 'TIMEOUT'); + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "enforce"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP support on ServiceWorker."); + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep_report_only)); + // As we don't want the service worker to control any page, generate a + // one-time scope. + const SCOPE = new URL(`resources/${token()}.html`, location).pathname; + const reg = + await service_worker_unregister_and_register(t, worker_url, SCOPE); + add_completion_callback(() => reg.unregister()); + const worker = reg.installing || reg.waiting || reg.active; + const mc = new MessageChannel(); + worker.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_not_equals(reports, 'TIMEOUT'); + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "reporting"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP-Report-Only support on ServiceWorker."); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-shared-worker.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-shared-worker.https.html new file mode 100644 index 0000000000..34af988fc6 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cache-storage-reporting-shared-worker.https.html @@ -0,0 +1,49 @@ +<!doctype html> +<html> +<head> + <title> + Check COEP report are send for CacheStorage requests in SharedWorker + </title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/common/utils.js"></script> + <script src="./resources/cache-storage-reporting.js"> </script> +</head> +<script> + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep)); + const worker = new SharedWorker(worker_url); + const mc = new MessageChannel(); + worker.port.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "enforce"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP support on SharedWorker.") + +promise_test(async (t) => { + const worker_url = local(encode(worker_path + header_coep_report_only)); + const worker = new SharedWorker(worker_url); + const mc = new MessageChannel(); + worker.port.postMessage({script: eval_script, port: mc.port2}, [mc.port2]); + const reports = (await new Promise(r => mc.port1.onmessage = r)).data; + assert_equals(reports.length, 1); + const report = reports[0]; + assert_equals(report.body.blockedURL, image_url); + assert_equals(report.body.type, "corp"); + assert_equals(report.body.disposition, "reporting"); + assert_equals(report.body.destination, ""); + assert_equals(report.type, "coep"); + assert_equals(report.url, worker_url); +}, "COEP-Report-Only support on SharedWorker.") + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-frame-javascript.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-frame-javascript.https.html new file mode 100644 index 0000000000..089019dc2e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-frame-javascript.https.html @@ -0,0 +1,25 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/script-factory.js"></script> +<div id=log></div> +<script> +async_test(t => { + window.addEventListener("message", t.step_func_done(({ data }) => { + assert_equals(data.id, ""); + assert_equals(data.origin, window.origin); + assert_true(data.sameOriginNoCORPSuccess); + assert_true(data.crossOriginNoCORPFailure, "Cross-origin without CORP did not fail"); + })); + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "resources/coep-frame.html"; + frame.onload = t.step_func(() => { + frame.onload = null; + frame.src = `javascript:${encodeURIComponent(createScript(window.origin, get_host_info().HTTPS_NOTSAMESITE_ORIGIN))}`; + }); + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy frame and javascript: URLs"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-on-response-from-service-worker.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-on-response-from-service-worker.https.html new file mode 100644 index 0000000000..b25b04ca4a --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/coep-on-response-from-service-worker.https.html @@ -0,0 +1,111 @@ +<!doctype html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const FRAME_URL = 'resources/coep-frame.html' +const SCOPE = new URL(FRAME_URL, location).pathname; +const SCRIPT = 'resources/sw.js?'; + +// This is similar to +// none-sw-from-require-corp.https.html, but there is one difference: +// In this file, the frame controlled by the service worker comes from +// the service worker, but on none-sw-from-require-corp.https.html +// the main document comes from the network directly. Hence the tests +// here test whether COEP is set correctly for documents coming from +// service workers. + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN + '/html/cross-origin-embedder-policy/'); +} + +let registration; +let frame; + +promise_test(async (t) => { + registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + await wait_for_state(t, registration.installing, 'activated') + frame = await with_iframe(FRAME_URL); +}, 'setup'); + + +promise_test(async (t) => { + const w = frame.contentWindow; + await w.fetch('resources/nothing-same-origin-corp.txt', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: same-origin'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await w.fetch('/common/blank.html', {mode: 'no-cors'}); +}, 'making a same-origin request for no CORP'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await w.fetch('resources/nothing-cross-origin-corp.js', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await promise_rejects_js( + t, w.TypeError, + w.fetch(remote('resources/nothing-same-origin-corp.txt'), {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await promise_rejects_js( + t, w.TypeError, w.fetch(remote('/common/blank.html'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await w.fetch( + remote('resources/nothing-cross-origin-corp.js'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await promise_rejects_js( + t, w.TypeError, + w.fetch(remote('resources/nothing-same-origin-corp.txt?passthrough'), + {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin [PASS THROUGH]'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await promise_rejects_js( + t, w.TypeError, + w.fetch(remote('/common/blank.html?passthrough'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP [PASS THROUGH]'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await w.fetch( + remote('resources/nothing-cross-origin-corp.js?passthrough'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin [PASS THROUGH]'); + +promise_test(async (t) => { + const w = frame.contentWindow; + await promise_rejects_js( + t, w.TypeError, w.fetch(remote('/common/blank.html'), {mode: 'cors'})); +}, 'making a cross-origin request with CORS without ACAO'); + +promise_test(async (t) => { + const w = frame.contentWindow; + const URL = remote( + '/common/blank.html?pipe=header(access-control-allow-origin,*'); + await w.fetch(URL, {mode: 'cors'}); +}, 'making a cross-origin request with CORS'); + +promise_test(async () => { + frame.remove(); + await registration.unregister(); +}, 'teardown'); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/META.yml b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/META.yml new file mode 100644 index 0000000000..2bf6754a6b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/META.yml @@ -0,0 +1,7 @@ +spec: To be defined. +suggested_reviewers: + - annevk + - arthursonzogni + - arturjanc + - camillelamy + - mikewest diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/README.md b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/README.md new file mode 100644 index 0000000000..86654525dd --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/README.md @@ -0,0 +1,3 @@ +# Related documents: +- https://github.com/mikewest/credentiallessness/ +- https://github.com/w3ctag/design-reviews/issues/582 diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache-storage.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache-storage.https.window.js new file mode 100644 index 0000000000..573e6ac9cb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache-storage.https.window.js @@ -0,0 +1,150 @@ +// META: timeout=long +// META: variant=?document +// META: variant=?dedicated_worker +// META: variant=?shared_worker +// META: variant=?service_worker +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +// Fetch a resource and store it into CacheStorage from |storer| context. Then +// check if it can be retrieved via CacheStorage.match from |retriever| context. +const cacheStorageTest = ( + description, + storer, + retriever, + resource_headers, + request_credential_mode, + expectation +) => { + promise_test_parallel(async test => { + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const url = cross_origin + "/common/square.png?pipe=" + resource_headers + + `&${token()}`; + const this_token = token(); + + // Fetch a request from |stored|. Store the opaque response into + // CacheStorage. + send(storer, ` + const cache = await caches.open("v1"); + const fetch_request = new Request("${url}", { + mode: 'no-cors', + credentials: '${request_credential_mode}' + }); + const fetch_response = await fetch(fetch_request); + await cache.put(fetch_request, fetch_response); + send("${this_token}", "stored"); + `); + assert_equals(await receive(this_token), "stored"); + + // Retrieved it from |retriever|. + send(retriever, ` + const cache = await caches.open("v1"); + try { + const response = await cache.match("${url}"); + send("${this_token}", "retrieved"); + } catch (error) { + send("${this_token}", "error"); + } + `); + assert_equals(await receive(this_token), expectation); + }, description); +}; + +// Execute the same set of tests for every type of execution contexts: +// Documents, DedicatedWorkers, SharedWorkers, and ServiceWorkers. The results +// should be independent of the context. +const environment = location.search.substr(1); +const constructor = environments[environment]; + +const context_none = constructor(coep_none)[0]; +const context_credentialless = constructor(coep_credentialless)[0]; +const context_require_corp = constructor(coep_require_corp)[0]; + +cacheStorageTest(`[${environment}] unsafe-none => unsafe-none`, + context_none, + context_none, + "", + "include", + "retrieved"); +cacheStorageTest(`[${environment}] unsafe-none => credentialless`, + context_none, + context_credentialless, + "", + "include", + "error"); +cacheStorageTest(`[${environment}] unsafe-none => credentialless (omit)`, + context_none, + context_credentialless, + "", + "omit", + "retrieved"); +cacheStorageTest(`[${environment}] unsafe-none => credentialless + CORP`, + context_none, + context_credentialless, + corp_cross_origin, + "include", + "retrieved"); +cacheStorageTest(`[${environment}] unsafe-none => require-corp`, + context_none, + context_require_corp, + "", + "include", + "error"); +cacheStorageTest(`[${environment}] unsafe-none => require-corp (omit)`, + context_none, + context_require_corp, + "", + "include", + "error"); +cacheStorageTest(`[${environment}] unsafe-none => require-corp + CORP`, + context_none, + context_require_corp, + corp_cross_origin, + "include", + "retrieved"); + +cacheStorageTest(`[${environment}] credentialless => unsafe-none`, + context_credentialless, + context_none, + "", + "include", + "retrieved"); +cacheStorageTest(`[${environment}] credentialless => credentialless`, + context_credentialless, + context_credentialless, + "", + "include", + "retrieved"); +cacheStorageTest(`[${environment}] credentialless => require-corp`, + context_credentialless, + context_require_corp, + "", + "include", + "error"); +cacheStorageTest(`[${environment}] credentialless => require-corp + CORP`, + context_credentialless, + context_require_corp, + corp_cross_origin, + "include", + "retrieved"); + +cacheStorageTest(`[${environment}] require_corp => unsafe-none`, + context_require_corp, + context_none, + corp_cross_origin, + "include", + "retrieved"); +cacheStorageTest(`[${environment}] require_corp => credentialless`, + context_require_corp, + context_credentialless, + corp_cross_origin, + "include", + "retrieved"); +cacheStorageTest(`[${environment}] require_corp => require-corp`, + context_require_corp, + context_require_corp, + corp_cross_origin, + "include", + "retrieved"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache.window.js new file mode 100644 index 0000000000..7d961804a0 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cache.window.js @@ -0,0 +1,84 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +// With COEP:credentialless, requesting a resource without credentials MUST NOT +// return a response requested with credentials. This would be a security +// issue, since COEP:credentialless can be used to enable crossOriginIsolation. +// +// The test the behavior of the HTTP cache: +// 1. b.com stores cookie. +// 2. a.com(COEP:unsafe-none): request b.com's resource. +// 3. a.com(COEP:credentialless): request b.com's resource. +// +// The first time, the resource is requested with credentials. The response is +// served with Cache-Control: max-age=31536000. It enters the cache. +// The second time, the resource is requested without credentials. The response +// in the cache must not be returned. + +const cookie_key = "coep_cache_key"; +const cookie_value = "coep_cache_value"; +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + +const GetCookie = (response) => { + return parseCookies(JSON.parse(response))[cookie_key]; +} + +// "same_origin" document with COEP:unsafe-none. +const w_coep_none_token = token(); +const w_coep_none_url = same_origin + executor_path + coep_none + + `&uuid=${w_coep_none_token}` +const w_coep_none = window.open(w_coep_none_url); +add_completion_callback(() => w_coep_none.close()); + +// "same_origin" document with COEP:credentialles. +const w_coep_credentialless_token = token(); +const w_coep_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_coep_credentialless_token}` +const w_coep_credentialless = window.open(w_coep_credentialless_url); +add_completion_callback(() => w_coep_credentialless.close()); + +const this_token = token(); + +// A request toward a "cross-origin" cacheable response. +const request_token = token(); +const request_url = cacheableShowRequestHeaders(cross_origin, request_token); + +promise_setup(async test => { + await setCookie(cross_origin, cookie_key, cookie_value + cookie_same_site_none); +}, "Set cookie"); + +// The "same-origin" COEP:unsafe-none document fetchs a "cross-origin" +// resource. The request is sent with credentials. +promise_setup(async test => { + send(w_coep_none_token, ` + await fetch("${request_url}", { + mode : "no-cors", + credentials: "include", + }); + send("${this_token}", "Resource fetched"); + `); + + assert_equals(await receive(this_token), "Resource fetched"); + assert_equals(await receive(request_token).then(GetCookie), cookie_value); +}, "Cache a response requested with credentials"); + +// The "same-origin" COEP:credentialless document fetches the same resource +// without credentials. The HTTP cache must not be used. Instead a second +// request must be made without credentials. +promise_test(async test => { + send(w_coep_credentialless_token, ` + await fetch("${request_url}", { + mode : "no-cors", + credentials: "include", + }); + send("${this_token}", "Resource fetched"); + `); + + assert_equals(await receive(this_token), "Resource fetched"); + + test.step_timeout(test.unreached_func("The HTTP cache has been used"), 1500); + assert_equals(await receive(request_token).then(GetCookie), undefined); +}, "The HTTP cache must not be used"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cross-origin-isolated.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cross-origin-isolated.window.js new file mode 100644 index 0000000000..361739f283 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/cross-origin-isolated.window.js @@ -0,0 +1,49 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +const http = get_host_info().HTTP_ORIGIN; +const https = get_host_info().HTTPS_ORIGIN; + +let crossOriginIsolatedTest = ( + description, + origin , + headers, + expect_crossOriginIsolated) => { + promise_test_parallel(async test => { + const w_token = token(); + const w_url = origin + executor_path + headers + `&uuid=${w_token}`; + const w = window.open(w_url) + add_completion_callback(() => w.close()); + + const this_token = token(); + send(w_token, ` + if (window.crossOriginIsolated) + send("${this_token}", "crossOriginIsolated"); + else + send("${this_token}", "not isolated") + `); + assert_equals(await receive(this_token), expect_crossOriginIsolated); + }, description); +} + +crossOriginIsolatedTest("Main crossOriginIsolated case:", + https, coop_same_origin + + coep_credentialless, "crossOriginIsolated"); + +crossOriginIsolatedTest("Missing HTTPS:", + http, coop_same_origin + + coep_credentialless, "not isolated"); + +crossOriginIsolatedTest("Missing COOP:same-origin:", + https, coep_credentialless, "not isolated"); + +crossOriginIsolatedTest("Report-only:", + https, coop_same_origin + + coep_report_only_credentialless, "not isolated"); + +crossOriginIsolatedTest("Report-only + enforced:", + https, coop_same_origin + + coep_credentialless + + coep_report_only_credentialless, "crossOriginIsolated"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/dedicated-worker.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/dedicated-worker.https.window.js new file mode 100644 index 0000000000..b06ec56210 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/dedicated-worker.https.window.js @@ -0,0 +1,137 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "credentialless_dedicated_worker"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +promise_test(async test => { + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let GetCookie = (response) => { + const headers_credentialless = JSON.parse(response); + return parseCookies(headers_credentialless)[cookie_key]; + } + + const dedicatedWorkerTest = function( + description, origin, coep_for_worker, + expected_cookies_control, + expected_cookies_credentialless) { + promise_test_parallel(async t => { + // Create workers for both window. + const worker_token_1 = token(); + const worker_token_2 = token(); + + // Used to check for errors creating the DedicatedWorker. + const worker_error_1 = token(); + const worker_error_2 = token(); + + const w_worker_src_1 = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${worker_token_1}`; + send(w_control_token, ` + new Worker("${w_worker_src_1}", {}); + worker.onerror = () => { + send("${worker_error_1}", "Worker blocked"); + } + `); + + const w_worker_src_2 = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${worker_token_2}`; + send(w_credentialless_token, ` + const worker = new Worker("${w_worker_src_2}", {}); + worker.onerror = () => { + send("${worker_error_2}", "Worker blocked"); + } + `); + + // Fetch resources with the workers. + const request_token_1 = token(); + const request_token_2 = token(); + const request_url_1 = showRequestHeaders(origin, request_token_1); + const request_url_2 = showRequestHeaders(origin, request_token_2); + + send(worker_token_1, ` + fetch("${request_url_1}", {mode: 'no-cors', credentials: 'include'}) + `); + send(worker_token_2, ` + fetch("${request_url_2}", {mode: 'no-cors', credentials: 'include'}); + `); + + const response_control = await Promise.race([ + receive(worker_error_1), + receive(request_token_1).then(GetCookie) + ]); + assert_equals(response_control, + expected_cookies_control, + "coep:none => "); + + const response_credentialless = await Promise.race([ + receive(worker_error_2), + receive(request_token_2).then(GetCookie) + ]); + assert_equals(response_credentialless, + expected_cookies_credentialless, + "coep:credentialless => "); + }, `fetch ${description}`) + }; + + dedicatedWorkerTest("same-origin + credentialless worker", + same_origin, coep_credentialless, + cookie_same_origin, + cookie_same_origin); + + dedicatedWorkerTest("same-origin + require_corp worker", + same_origin, coep_require_corp, + cookie_same_origin, + cookie_same_origin); + + dedicatedWorkerTest("same-origin", + same_origin, coep_none, + cookie_same_origin, + "Worker blocked"); + + dedicatedWorkerTest("cross-origin", + cross_origin, coep_none, + cookie_cross_origin, + "Worker blocked" // Owner's policy is credentialles, so we can't + // create a worker with coep_none. + ); + + dedicatedWorkerTest("cross-origin + credentialless worker", + cross_origin, coep_credentialless, + undefined, // Worker created successfully with credentialless, and fetch doesn't get credentials + undefined // Worker created successfully with credentialless, and fetch doesn't get credentials + ); + + dedicatedWorkerTest("cross-origin + require_corp worker", + cross_origin, coep_require_corp, + cookie_cross_origin, + cookie_cross_origin // The worker's policy is require_corp and doing a + // fetch within it has nothing to do with the Owner's policy. + ); +}) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/fetch.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/fetch.https.window.js new file mode 100644 index 0000000000..6ea94d0a19 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/fetch.https.window.js @@ -0,0 +1,127 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +promise_test(async test => { + const same_origin = get_host_info().HTTPS_ORIGIN; + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const cookie_key = "coep_credentialless_fetch"; + const cookie_same_origin = "same_origin"; + const cookie_cross_origin = "cross_origin"; + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + const fetchTest = function( + description, origin, mode, credentials, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async test => { + const token_1 = token(); + const token_2 = token(); + + send(w_control_token, ` + fetch("${showRequestHeaders(origin, token_1)}", { + mode:"${mode}", + credentials: "${credentials}", + }); + `); + send(w_credentialless_token, ` + fetch("${showRequestHeaders(origin, token_2)}", { + mode:"${mode}", + credentials: "${credentials}", + }); + `); + + const headers_control = JSON.parse(await receive(token_1)); + const headers_credentialless = JSON.parse(await receive(token_2)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + }, `fetch ${description}`) + }; + + // Cookies are never sent with credentials='omit' + fetchTest("same-origin + no-cors + credentials:omit", + same_origin, 'no-cors', 'omit', + undefined, + undefined); + fetchTest("same-origin + cors + credentials:omit", + same_origin, 'cors', 'omit', + undefined, + undefined); + fetchTest("cross-origin + no-cors + credentials:omit", + cross_origin, 'no-cors', 'omit', + undefined, + undefined); + fetchTest("cross-origin + cors + credentials:omit", + cross_origin, 'cors', 'omit', + undefined, + undefined); + + // Same-origin request contains Cookies. + fetchTest("same-origin + no-cors + credentials:include", + same_origin, 'no-cors', 'include', + cookie_same_origin, + cookie_same_origin); + fetchTest("same-origin + cors + credentials:include", + same_origin, 'cors', 'include', + cookie_same_origin, + cookie_same_origin); + fetchTest("same-origin + no-cors + credentials:same-origin", + same_origin, 'no-cors', 'same-origin', + cookie_same_origin, + cookie_same_origin); + fetchTest("same-origin + cors + credentials:same-origin", + same_origin, 'cors', 'same-origin', + cookie_same_origin, + cookie_same_origin); + + // Cross-origin CORS requests contains Cookies, if credentials mode is set to + // 'include'. This does not depends on COEP. + fetchTest("cross-origin + cors + credentials:include", + cross_origin, 'cors', 'include', + cookie_cross_origin, + cookie_cross_origin); + fetchTest("cross-origin + cors + same-origin-credentials", + cross_origin, 'cors', 'same-origin', + undefined, + undefined); + + // Cross-origin no-CORS requests includes Cookies when: + // 1. credentials mode is 'include' + // 2. COEP: is not credentialless. + fetchTest("cross-origin + no-cors + credentials:include", + cross_origin, 'no-cors', 'include', + cookie_cross_origin, + undefined); + + fetchTest("cross-origin + no-cors + credentials:same-origin", + cross_origin, 'no-cors', 'same-origin', + undefined, + undefined); +}, ""); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-credentialless.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-credentialless.https.window.js new file mode 100644 index 0000000000..f9d9fcb932 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-credentialless.https.window.js @@ -0,0 +1,37 @@ +// META: variant=?1-4 +// META: variant=?5-9 +// META: variant=?9-last +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=./resources/iframeTest.js +// META: script=/common/subset-tests.js + +const parent_coep_credentialless = newWindow(coep_credentialless); +subsetTest(iframeTest, "COEP:credentialless embeds same-origin COEP:none", + parent_coep_credentialless, same_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:credentialless embeds cross-origin COEP:none", + parent_coep_credentialless, cross_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:credentialless embeds same-origin COEP:credentialless", + parent_coep_credentialless, same_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:credentialless embeds cross-origin COEP:credentialless", + parent_coep_credentialless, cross_origin, coep_credentialless, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:credentialless embeds same-origin COEP:require-corp", + parent_coep_credentialless, same_origin, coep_require_corp, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:credentialless embeds cross-origin COEP:require-corp", + parent_coep_credentialless, cross_origin, coep_require_corp, EXPECT_BLOCK); + +// Using CORP:cross-origin might unblock previously blocked iframes. +subsetTest(iframeTestCORP, "COEP:credentialless embeds same-origin COEP:none", + parent_coep_credentialless, same_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTestCORP, "COEP:credentialless embeds cross-origin COEP:none", + parent_coep_credentialless, cross_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTestCORP, "COEP:credentialless embeds same-origin COEP:credentialless", + parent_coep_credentialless, same_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:credentialless embeds cross-origin COEP:credentialless", + parent_coep_credentialless, cross_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:credentialless embeds same-origin COEP:require-corp", + parent_coep_credentialless, same_origin, coep_require_corp, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:credentialless embeds cross-origin COEP:require-corp", + parent_coep_credentialless, cross_origin, coep_require_corp, EXPECT_LOAD); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-none.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-none.https.window.js new file mode 100644 index 0000000000..4f50b8d407 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-none.https.window.js @@ -0,0 +1,22 @@ +// META: variant=?1-4 +// META: variant=?5-last +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=./resources/iframeTest.js +// META: script=/common/subset-tests.js + +const parent_coep_none = newWindow(coep_none); +subsetTest(iframeTest, "COEP:none embeds same-origin COEP:none", + parent_coep_none, same_origin, coep_none, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:none embeds cross-origin COEP:none", + parent_coep_none, cross_origin, coep_none, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:none embeds same-origin COEP:credentialless", + parent_coep_none, same_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:none embeds cross-origin COEP:credentialless", + parent_coep_none, cross_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:none embeds same-origin COEP:require-corp", + parent_coep_none, same_origin, coep_require_corp, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:none embeds cross-origin COEP:require-corp", + parent_coep_none, cross_origin, coep_require_corp, EXPECT_LOAD); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-redirect.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-redirect.https.window.js new file mode 100644 index 0000000000..a9f6a1c88e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-redirect.https.window.js @@ -0,0 +1,41 @@ +// META: variant=?1-4 +// META: variant=?5-last +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=./resources/iframeTest.js +promise_test(async test => { + const test_token = token(); + const child_token = token(); + + const redirecting_child_url = cross_origin + "/common/dispatcher/executor.html?uuid=" + child_token; + + const child_url = + same_origin + + "/html/cross-origin-embedder-policy/credentialless/resources/redirect_none_to_credentialless.py?redirectTo=" + redirecting_child_url; + + await send(newWindow(coep_credentialless), ` + let iframe = document.createElement("iframe"); + iframe.src = "${child_url}"; + document.body.appendChild(iframe); + `); + + await send(child_token, ` + send("${test_token}", "load"); + `); + + // There are no interoperable ways to check an iframe failed to load. So a + // timeout is being used. + // See https://github.com/whatwg/html/issues/125 + // Use a shorter timeout when it is expected to be reached. + // - The long delay reduces the false-positive rate. False-positive causes + // stability problems on bot, so a big delay is used to vanish them. + // https://crbug.com/1215956. + // - The short delay avoids delaying too much the test(s) for nothing and + // timing out. False-negative are not a problem, they just need not to + // overwhelm the true-negative, which is trivial to get. + step_timeout(()=>send(test_token, "block"), 6000); + + assert_equals(await receive(test_token), EXPECT_LOAD); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-require-corp.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-require-corp.https.window.js new file mode 100644 index 0000000000..a70d4ff8fc --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe-coep-require-corp.https.window.js @@ -0,0 +1,38 @@ +// META: variant=?1-4 +// META: variant=?5-9 +// META: variant=?9-last +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=./resources/iframeTest.js +// META: script=/common/subset-tests.js + +const parent_coep_require_corp = newWindow(coep_require_corp); + +subsetTest(iframeTest, "COEP:require-corp embeds same-origin COEP:none", + parent_coep_require_corp, same_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:require-corp embeds cross-origin COEP:none", + parent_coep_require_corp, cross_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:require-corp embeds same-origin COEP:credentialless", + parent_coep_require_corp, same_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:require-corp embeds cross-origin COEP:credentialless", + parent_coep_require_corp, cross_origin, coep_credentialless, EXPECT_BLOCK); +subsetTest(iframeTest, "COEP:require-corp embeds same-origin COEP:require-corp", + parent_coep_require_corp, same_origin, coep_require_corp, EXPECT_LOAD); +subsetTest(iframeTest, "COEP:require-corp embeds cross-origin COEP:require-corp", + parent_coep_require_corp, cross_origin, coep_require_corp, EXPECT_BLOCK); + +// Using CORP:cross-origin might unblock previously blocked iframes. +subsetTest(iframeTestCORP, "COEP:require-corp embeds same-origin COEP:none", + parent_coep_require_corp, same_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTestCORP, "COEP:require-corp embeds cross-origin COEP:none", + parent_coep_require_corp, cross_origin, coep_none, EXPECT_BLOCK); +subsetTest(iframeTestCORP, "COEP:require-corp embeds same-origin COEP:credentialless", + parent_coep_require_corp, same_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:require-corp embeds cross-origin COEP:credentialless", + parent_coep_require_corp, cross_origin, coep_credentialless, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:require-corp embeds same-origin COEP:require-corp", + parent_coep_require_corp, same_origin, coep_require_corp, EXPECT_LOAD); +subsetTest(iframeTestCORP, "COEP:require-corp embeds cross-origin COEP:require-corp", + parent_coep_require_corp, cross_origin, coep_require_corp, EXPECT_LOAD); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe.window.js new file mode 100644 index 0000000000..d7a9c1e170 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/iframe.window.js @@ -0,0 +1,47 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "coep_redirect"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +// Operate on a window with COEP:credentialless. +const w_token = token(); +const w_url = same_origin + executor_path + coep_credentialless + + `&uuid=${w_token}` +const w = window.open(w_url); +add_completion_callback(() => w.close()); + +// Check whether COEP:credentialless applies to navigation request. It +// shouldn't. +const iframeTest = function(name, origin, expected_cookies) { + promise_test_parallel(async test => { + const token_request = token(); + const url = showRequestHeaders(origin, token_request); + + send(w_token, ` + const iframe = document.createElement("iframe"); + iframe.src = "${url}"; + document.body.appendChild(iframe); + `); + + const headers = JSON.parse(await receive(token_request)); + assert_equals(parseCookies(headers)[cookie_key], expected_cookies); + }, name) +}; + +promise_test_parallel(async test => { + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + iframeTest("same-origin", same_origin, cookie_same_origin); + iframeTest("cross-origin", cross_origin, cookie_cross_origin); +}, "Setup"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/image.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/image.https.window.js new file mode 100644 index 0000000000..2e9166d1bb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/image.https.window.js @@ -0,0 +1,97 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +promise_test_parallel(async test => { + const same_origin = get_host_info().HTTPS_ORIGIN; + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const cookie_key = "coep_credentialless_image"; + const cookie_same_origin = "same_origin"; + const cookie_cross_origin = "cross_origin"; + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let imgTest = function( + description, origin, mode, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async test => { + const token_1 = token(); + const token_2 = token(); + + send(w_control_token, ` + let img = document.createElement("img"); + img.src = "${showRequestHeaders(origin, token_1)}"; + ${mode}; + document.body.appendChild(img); + `); + send(w_credentialless_token, ` + let img = document.createElement("img"); + img.src = "${showRequestHeaders(origin, token_2)}"; + ${mode}; + document.body.appendChild(img); + `); + + const headers_control = JSON.parse(await receive(token_1)); + const headers_credentialless = JSON.parse(await receive(token_2)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + }, `image ${description}`) + }; + + // Same-origin request always contains Cookies: + imgTest("same-origin + undefined", + same_origin, '', + cookie_same_origin, + cookie_same_origin); + imgTest("same-origin + anonymous", + same_origin, 'img.crossOrigin="anonymous"', + cookie_same_origin, + cookie_same_origin); + imgTest("same-origin + use-credentials", + same_origin, 'img.crossOrigin="use-credentials"', + cookie_same_origin, + cookie_same_origin); + + // Cross-origin request contains cookies in the following cases: + // - COEP:credentialless is not set. + // - img.crossOrigin is `use-credentials`. + imgTest("cross-origin + undefined", + cross_origin, '', + cookie_cross_origin, + undefined); + imgTest("cross-origin + anonymous", + cross_origin, 'img.crossOrigin="anonymous"', + undefined, + undefined); + imgTest("cross-origin + use-credentials", + cross_origin, 'img.crossOrigin="use-credentials"', + cookie_cross_origin, + cookie_cross_origin); +}, "Main"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/link.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/link.https.window.js new file mode 100644 index 0000000000..0a0f8eef66 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/link.https.window.js @@ -0,0 +1,99 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +promise_test_parallel(async test => { + const same_origin = get_host_info().HTTPS_ORIGIN; + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const cookie_key = "coep_credentialless_link"; + const cookie_same_origin = "same_origin"; + const cookie_cross_origin = "cross_origin"; + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let linkTest = function( + description, origin, mode, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async test => { + const token_1 = token(); + const token_2 = token(); + + send(w_control_token, ` + let link = document.createElement("link"); + link.href = "${showRequestHeaders(origin, token_1)}"; + link.rel = "stylesheet"; + ${mode} + document.head.appendChild(link); + `); + send(w_credentialless_token, ` + let link = document.createElement("link"); + link.href = "${showRequestHeaders(origin, token_2)}"; + link.rel = "stylesheet"; + ${mode} + document.head.appendChild(link); + `); + + const headers_control = JSON.parse(await receive(token_1)); + const headers_credentialless = JSON.parse(await receive(token_2)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + }, `link ${description}`) + }; + + // Same-origin request always contains Cookies: + linkTest("same-origin + undefined", + same_origin, '', + cookie_same_origin, + cookie_same_origin); + linkTest("same-origin + anonymous", + same_origin, 'link.crossOrigin="anonymous"', + cookie_same_origin, + cookie_same_origin); + linkTest("same-origin + use-credentials", + same_origin, 'link.crossOrigin="use-credentials"', + cookie_same_origin, + cookie_same_origin); + + // Cross-origin request contains cookies in the following cases: + // - COEP:credentialless is not set. + // - link.crossOrigin is `use-credentials`. + linkTest("cross-origin + undefined", + cross_origin, '', + cookie_cross_origin, + undefined); + linkTest("cross-origin + anonymous", + cross_origin, 'link.crossOrigin="anonymous"', + undefined, + undefined); + linkTest("cross-origin + use-credentials", + cross_origin, 'link.crossOrigin="use-credentials"', + cookie_cross_origin, + cookie_cross_origin); +}, "Main"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/redirect.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/redirect.window.js new file mode 100644 index 0000000000..db8ca08d36 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/redirect.window.js @@ -0,0 +1,55 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "coep_redirect"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +// Operate on a window with COEP:credentialless.: +const w_token = token(); +const w_url = same_origin + executor_path + coep_credentialless + + `&uuid=${w_token}` +const w = window.open(w_url); +add_completion_callback(() => w.close()); + +let redirectTest = function(name, + redirect_origin, + final_origin, + expected_cookies) { + promise_test_parallel(async test => { + const token_request = token(); + const url = redirect_origin + "/common/redirect.py?location=" + + encodeURIComponent(showRequestHeaders(final_origin, token_request)); + + send(w_token, ` + const img = document.createElement("img"); + img.src = "${url}"; + document.body.appendChild(img); + `); + + const headers = JSON.parse(await receive(token_request)); + assert_equals(parseCookies(headers)[cookie_key], expected_cookies); + }, name) +}; + +promise_test_parallel(async test => { + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + redirectTest("same-origin -> same-origin", + same_origin, same_origin, cookie_same_origin); + redirectTest("same-origin -> cross-origin", + same_origin, cross_origin, undefined) + redirectTest("cross-origin -> same-origin", + cross_origin, same_origin, undefined); + redirectTest("cross-origin -> cross-origin", + cross_origin, cross_origin, undefined); +}, "Setup"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-navigation.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-navigation.https.window.js new file mode 100644 index 0000000000..1d62996e38 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-navigation.https.window.js @@ -0,0 +1,139 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=./resources/common.js +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const COEP = '|header(cross-origin-embedder-policy,credentialless)'; +const COEP_RO = + '|header(cross-origin-embedder-policy-report-only,credentialless)'; +const CORP_CROSS_ORIGIN = + '|header(cross-origin-resource-policy,cross-origin)'; +const FRAME_URL = `${ORIGIN}/common/blank.html?pipe=`; +const REMOTE_FRAME_URL = `${REMOTE_ORIGIN}/common/blank.html?pipe=`; + +function checkCorpReport(report, contextUrl, blockedUrl, disposition) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'corp'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); + assert_equals(report.body.destination, 'iframe'); +} + +function checkCoepMismatchReport(report, contextUrl, blockedUrl, disposition) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'navigation'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); +} + +function loadFrame(document, url) { + return new Promise((resolve, reject) => { + const frame = document.createElement('iframe'); + frame.src = url; + frame.onload = () => resolve(frame); + frame.onerror = reject; + document.body.appendChild(frame); + }); +} + +// |parentSuffix| is a suffix for the parent frame URL. +// |targetUrl| is a URL for the target frame. +async function loadFrames(test, parentSuffix, targetUrl) { + const frame = await loadFrame(document, FRAME_URL + parentSuffix); + test.add_cleanup(() => frame.remove()); + // Here we don't need "await". This loading may or may not succeed, and + // we're not interested in the result. + loadFrame(frame.contentDocument, targetUrl); + + return frame; +} + +async function observeReports(global, expected_count) { + const reports = []; + const receivedEveryReports = new Promise(resolve => { + if (expected_count == 0) + resolve(); + + const observer = new global.ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + if (expected_count <= reports.length) + resolve(); + }); + observer.observe(); + + }); + + // Wait 500ms more to catch additionnal unexpected reports. + await receivedEveryReports; + await new Promise(r => step_timeout(r, 500)); + return reports; +} + +function desc(headers) { + return headers === '' ? '(none)' : headers; +} + +// CASES is a list of test case. Each test case consists of: +// parent_headers: the suffix of the URL of the parent frame. +// target_headers: the suffix of the URL of the target frame. +// expected_reports: one of: +// 'CORP': CORP violation +// 'CORP-RO': CORP violation (report only) +// 'NAV': COEP mismatch between the frames. +// 'NAV-RO': COEP mismatch between the frames (report only). +const reportingTest = function( + parent_headers, target_headers, expected_reports) { + // These tests are very slow, so they must be run in parallel using + // async_test. + promise_test_parallel(async t => { + const targetUrl = REMOTE_FRAME_URL + target_headers; + const parent = await loadFrames(t, parent_headers, targetUrl); + const contextUrl = parent.src ? parent.src : 'about:blank'; + const reports = await observeReports( + parent.contentWindow, + expected_reports.length + ); + assert_equals(reports.length, expected_reports.length); + for (let i = 0; i < reports.length; i += 1) { + const report = reports[i]; + switch (expected_reports[i]) { + case 'CORP': + checkCorpReport(report, contextUrl, targetUrl, 'enforce'); + break; + case 'CORP-RO': + checkCorpReport(report, contextUrl, targetUrl, 'reporting'); + break; + case 'NAV': + checkCoepMismatchReport(report, contextUrl, targetUrl, 'enforce'); + break; + case 'NAV-RO': + checkCoepMismatchReport(report, contextUrl, targetUrl, 'reporting'); + break; + default: + assert_unreached( + 'Unexpected report exception: ' + expected_reports[i]); + } + } + }, `parent: ${desc(parent_headers)}, target: ${desc(target_headers)}, `); +} + +reportingTest('', '', []); +reportingTest('', COEP, []); +reportingTest(COEP, COEP, ['CORP']); +reportingTest(COEP, '', ['CORP']); + +reportingTest('', CORP_CROSS_ORIGIN, []); +reportingTest(COEP, CORP_CROSS_ORIGIN, ['NAV']); + +reportingTest('', COEP + CORP_CROSS_ORIGIN, []); +reportingTest(COEP, COEP + CORP_CROSS_ORIGIN, []); + +reportingTest(COEP_RO, COEP, ['CORP-RO']); +reportingTest(COEP_RO, '', ['CORP-RO', 'NAV-RO']); +reportingTest(COEP_RO, CORP_CROSS_ORIGIN, ['NAV-RO']); +reportingTest(COEP_RO, COEP + CORP_CROSS_ORIGIN, []); + +reportingTest(COEP, COEP_RO + CORP_CROSS_ORIGIN, ['NAV']); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-subresource-corp.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-subresource-corp.https.window.js new file mode 100644 index 0000000000..ab583fd49e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/reporting-subresource-corp.https.window.js @@ -0,0 +1,74 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const BASE = "/html/cross-origin-embedder-policy/resources"; +const REPORTING_FRAME_URL = `${ORIGIN}${BASE}/reporting-empty-frame.html` + + '?pipe=header(cross-origin-embedder-policy,credentialless)' + + '&token=${token()}'; + +async function observeReports(global, expected_count) { + const reports = []; + const receivedEveryReports = new Promise(resolve => { + if (expected_count == 0) + resolve(); + + const observer = new global.ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + if (expected_count <= reports.length) + resolve(); + }); + observer.observe(); + + }); + + await receivedEveryReports; + // Wait 1000ms more to catch additionnal unexpected reports. + await new Promise(r => step_timeout(r, 1000)); + return reports; +} + +async function fetchInFrame(t, frameUrl, url, expected_count) { + const frame = await with_iframe(frameUrl); + t.add_cleanup(() => frame.remove()); + + const init = { mode: 'no-cors', cache: 'no-store' }; + let future_reports = observeReports(frame.contentWindow, expected_count); + await frame.contentWindow.fetch(url, init).catch(() => {}); + + return await future_reports; +} + +function checkReport(report, contextUrl, blockedUrl, disposition, destination) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'corp'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); + assert_equals(report.body.destination, destination); +} + +// A redirection is used, so that the initial request is same-origin and is +// proxyied through the service worker. The ServiceWorker is COEP:unsafe-none, +// so it will make the cross-origin request with credentials. The fetch will +// succeed, but the response will be blocked by CORP when entering the +// COEP:credentialless document. +// https://github.com/w3c/ServiceWorker/issues/1592 +promise_test(async (t) => { + const url = `${ORIGIN}/common/redirect.py?location=` + + encodeURIComponent(`${REMOTE_ORIGIN}/common/text-plain.txt`); + const WORKER_URL = `${ORIGIN}${BASE}/sw.js`; + const reg = await service_worker_unregister_and_register( + t, WORKER_URL, REPORTING_FRAME_URL); + t.add_cleanup(() => reg.unregister()); + const worker = reg.installing || reg.waiting || reg.active; + worker.addEventListener('error', t.unreached_func('Worker.onerror')); + await wait_for_state(t, worker, 'activated'); + + const reports = await fetchInFrame(t, REPORTING_FRAME_URL, url, 1); + assert_equals(reports.length, 1); + checkReport(reports[0], REPORTING_FRAME_URL, url, 'enforce', ''); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/common.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/common.js new file mode 100644 index 0000000000..ce21c766f6 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/common.js @@ -0,0 +1,134 @@ +const executor_path = '/common/dispatcher/executor.html?pipe='; +const executor_worker_path = '/common/dispatcher/executor-worker.js?pipe='; +const executor_service_worker_path = '/common/dispatcher/executor-service-worker.js?pipe='; + +// COEP +const coep_none = + '|header(Cross-Origin-Embedder-Policy,none)'; +const coep_credentialless = + '|header(Cross-Origin-Embedder-Policy,credentialless)'; +const coep_require_corp = + '|header(Cross-Origin-Embedder-Policy,require-corp)'; + +// COEP-Report-Only +const coep_report_only_credentialless = + '|header(Cross-Origin-Embedder-Policy-Report-Only,credentialless)'; + +// COOP +const coop_same_origin = + '|header(Cross-Origin-Opener-Policy,same-origin)'; + +// CORP +const corp_cross_origin = + '|header(Cross-Origin-Resource-Policy,cross-origin)'; + +const cookie_same_site_none = ';SameSite=None;Secure'; + +// Test using the modern async/await primitives are easier to read/write. +// However they run sequentially, contrary to async_test. This is the parallel +// version, to avoid timing out. +let promise_test_parallel = (promise, description) => { + async_test(test => { + promise(test) + .then(() => test.done()) + .catch(test.step_func(error => { throw error; })); + }, description); +}; + +// Add a cookie |cookie_key|=|cookie_value| on an |origin|. +// Note: cookies visibility depends on the path of the document. Those are set +// from a document from: /html/cross-origin-embedder-policy/credentialless/. So +// the cookie is visible to every path underneath. +const setCookie = async (origin, cookie_key, cookie_value) => { + const popup_token = token(); + const popup_url = origin + executor_path + `&uuid=${popup_token}`; + const popup = window.open(popup_url); + + const reply_token = token(); + send(popup_token, ` + document.cookie = "${cookie_key}=${cookie_value}"; + send("${reply_token}", "done"); + `); + assert_equals(await receive(reply_token), "done"); + popup.close(); +} + +let parseCookies = function(headers_json) { + if (!headers_json["cookie"]) + return {}; + + return headers_json["cookie"] + .split(';') + .map(v => v.split('=')) + .reduce((acc, v) => { + acc[v[0].trim()] = v[1].trim(); + return acc; + }, {}); +} + +// Open a new window with a given |origin|, loaded with COEP:credentialless. The +// new document will execute any scripts sent toward the token it returns. +const newCredentiallessWindow = (origin) => { + const main_document_token = token(); + const url = origin + executor_path + coep_credentialless + + `&uuid=${main_document_token}`; + const context = window.open(url); + add_completion_callback(() => w.close()); + return main_document_token; +}; + +// Create a new iframe, loaded with COEP:credentialless. +// The new document will execute any scripts sent toward the token it returns. +const newCredentiallessIframe = (parent_token, child_origin) => { + const sub_document_token = token(); + const iframe_url = child_origin + executor_path + coep_credentialless + + `&uuid=${sub_document_token}`; + send(parent_token, ` + let iframe = document.createElement("iframe"); + iframe.src = "${iframe_url}"; + document.body.appendChild(iframe); + `) + return sub_document_token; +}; + +// A common interface for building the 4 type of execution contexts: +// It outputs: [ +// - The token to communicate with the environment. +// - A promise resolved when the environment encounters an error. +// ] +const environments = { + document: headers => { + const tok = token(); + const url = window.origin + executor_path + headers + `&uuid=${tok}`; + const context = window.open(url); + add_completion_callback(() => context.close()); + return [tok, new Promise(resolve => {})]; + }, + + dedicated_worker: headers => { + const tok = token(); + const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`; + const context = new Worker(url); + return [tok, new Promise(resolve => context.onerror = resolve)]; + }, + + shared_worker: headers => { + const tok = token(); + const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`; + const context = new SharedWorker(url); + return [tok, new Promise(resolve => context.onerror = resolve)]; + }, + + service_worker: headers => { + const tok = token(); + const url = window.origin + executor_worker_path + headers + `&uuid=${tok}`; + const scope = url; // Generate a one-time scope for service worker. + const error = new Promise(resolve => { + navigator.serviceWorker.register(url, {scope: scope}) + .then(registration => { + add_completion_callback(() => registration.unregister()); + }, /* catch */ resolve); + }); + return [tok, error]; + }, +}; diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/iframeTest.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/iframeTest.js new file mode 100644 index 0000000000..501a864d46 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/iframeTest.js @@ -0,0 +1,85 @@ +// One document embeds another in an iframe. Both are loaded from the network. +// Depending on the response headers: +// - Cross-Origin-Embedder-Policy (COEP) +// - Cross-Origin-Resource-Policy (CORP) +// The child must load or must be blocked. +// +// What to do for: +// - COEP:credentialless +// - COEP:credentialless-on-children +// is currently an active open question. This test will be updated/completed +// later. + +// There are no interoperable ways to check an iframe failed to load. So a +// timeout is being used. See https://github.com/whatwg/html/issues/125 +// Moreover, we want to track progress, managing timeout explicitly allows to +// get a per-test results, even in case of failure of one. +setup({ explicit_timeout: true }); + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + +// Open a new window loaded with the given |headers|. The new document will +// execute any script sent toward the token it returns. +const newWindow = (headers) => { + const executor_token = token(); + const url = same_origin + executor_path + headers + `&uuid=${executor_token}`; + const w = window.open(url); + add_completion_callback(() => w.close()); + return executor_token; +}; + +const EXPECT_LOAD = "load"; +const EXPECT_BLOCK = "block"; + +// Load in iframe. Control both the parent and the child headers. Check whether +// it loads or not. +const iframeTest = function( + description, + parent_token, + child_origin, + child_headers, + expectation +) { + promise_test_parallel(async test => { + const test_token = token(); + + const child_token = token(); + const child_url = child_origin + executor_path + child_headers + + `&uuid=${child_token}`; + + await send(parent_token, ` + let iframe = document.createElement("iframe"); + iframe.src = "${child_url}"; + document.body.appendChild(iframe); + `); + + await send(child_token, ` + send("${test_token}", "load"); + `); + + // There are no interoperable ways to check an iframe failed to load. So a + // timeout is being used. + // See https://github.com/whatwg/html/issues/125 + // Use a shorter timeout when it is expected to be reached. + // - The long delay reduces the false-positive rate. False-positive causes + // stability problems on bot, so a big delay is used to vanish them. + // https://crbug.com/1215956. + // - The short delay avoids delaying too much the test(s) for nothing and + // timing out. False-negative are not a problem, they just need not to + // overwhelm the true-negative, which is trivial to get. + step_timeout(()=>send(test_token, "block"), expectation == EXPECT_BLOCK + ? 2000 + : 6000 + ); + + assert_equals(await receive(test_token), expectation); + }, description); +} + +// A decorated version of iframeTest, adding CORP:cross-origin to the child. +const iframeTestCORP = function() { + arguments[0] += ", CORP:cross-origin"; // description + arguments[3] += corp_cross_origin; // child_headers + iframeTest(...arguments); +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/redirect_none_to_credentialless.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/redirect_none_to_credentialless.py new file mode 100644 index 0000000000..7f0c35e93e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/resources/redirect_none_to_credentialless.py @@ -0,0 +1,14 @@ +COEP_HEADER = b"|header(cross-origin-embedder-policy, credentialless)" +CORP_HEADER = b"|header(cross-origin-resource-policy, cross-origin)" + +def main(request, response): + """ + Causes a redirection that the initial response doesn't have credentialless and the + redirected response has credentialless. + """ + response.status = 302 + + location = \ + request.GET[b'redirectTo'] + b"&pipe=" + COEP_HEADER + CORP_HEADER + + response.headers.set(b"Location", location) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/script.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/script.https.window.js new file mode 100644 index 0000000000..96bf7b08db --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/script.https.window.js @@ -0,0 +1,99 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +window.onload = function() { + promise_test_parallel(async test => { + const same_origin = get_host_info().HTTPS_ORIGIN; + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const cookie_key = "coep_credentialless_script"; + const cookie_same_origin = "same_origin"; + const cookie_cross_origin = "cross_origin"; + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let scriptTest = function( + description, origin, mode, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async test => { + const token_1 = token(); + const token_2 = token(); + + send(w_control_token, ` + let script = document.createElement("script"); + script.src = "${showRequestHeaders(origin, token_1)}"; + ${mode}; + document.body.appendChild(script); + `); + send(w_credentialless_token, ` + let script = document.createElement("script"); + script.src = "${showRequestHeaders(origin, token_2)}"; + ${mode}; + document.body.appendChild(script); + `); + + const headers_control = JSON.parse(await receive(token_1)); + const headers_credentialless = JSON.parse(await receive(token_2)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + }, `script ${description}`) + }; + + // Same-origin request always contains Cookies: + scriptTest("same-origin + undefined", + same_origin, '', + cookie_same_origin, + cookie_same_origin); + scriptTest("same-origin + anonymous", + same_origin, 'script.crossOrigin="anonymous"', + cookie_same_origin, + cookie_same_origin); + scriptTest("same-origin + use-credentials", + same_origin, 'script.crossOrigin="use-credentials"', + cookie_same_origin, + cookie_same_origin); + + // Cross-origin request contains cookies in the following cases: + // - COEP:credentialless is not set. + // - script.crossOrigin is `use-credentials`. + scriptTest("cross-origin + undefined", + cross_origin, '', + cookie_cross_origin, + undefined); + scriptTest("cross-origin + anonymous", + cross_origin, 'script.crossOrigin="anonymous"', + undefined, + undefined); + scriptTest("cross-origin + use-credentials", + cross_origin, 'script.crossOrigin="use-credentials"', + cookie_cross_origin, + cookie_cross_origin); + }, "Main"); +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-credentialless-proxy.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-credentialless-proxy.https.window.js new file mode 100644 index 0000000000..d1a61dbb57 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-credentialless-proxy.https.window.js @@ -0,0 +1,85 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + +promise_test(async test => { + const this_token_1 = token(); + const this_token_2 = token(); + + // Register a COEP:credentialless ServiceWorker. + const sw_token = token(); + const sw_url = + executor_service_worker_path + coep_credentialless + `&uuid=${sw_token}`; + // Executors should be controlled by the service worker. + const scope = executor_path; + const sw_registration = + await service_worker_unregister_and_register(test, sw_url, scope); + test.add_cleanup(() => sw_registration.unregister()); + await wait_for_state(test, sw_registration.installing, 'activated'); + + // Configure the ServiceWorker to proxy the fetch requests. Wait for the + // worker to be installed and activated. + send(sw_token, ` + fetchHandler = event => { + if (!event.request.url.includes("/proxied")) + return; + + send("${this_token_1}", "ServiceWorker: Proxying"); + + // Response with a cross-origin no-cors resource. + const url = "${cross_origin}" + "/common/blank.html}"; + + event.respondWith(new Promise(async resolve => { + try { + let response = await fetch(url, { + mode: "no-cors", + credentials: "include" + }); + send("${this_token_1}", "ServiceWorker: Fetch success"); + resolve(response); + } catch (error) { + send("${this_token_1}", "ServiceWorker: Fetch failure"); + resolve(new Response("", {status: 400})); + } + })); + } + + await clients.claim(); + + send("${this_token_1}", serviceWorker.state); + `) + assert_equals(await receive(this_token_1), "activated"); + + // Create a COEP:credentialless document. + const document_token = environments["document"](coep_credentialless)[0]; + + // The document fetches a same-origin no-cors resource. The requests needs to + // be same-origin to be handled by the ServiceWorker. + send(document_token, ` + try { + const response = await fetch("/proxied", { mode: "no-cors", }); + + send("${this_token_2}", "Document: Fetch success"); + } catch (error) { + send("${this_token_2}", "Document: Fetch error"); + } + `); + + // The COEP:credentialless ServiceWorker is able to handle the cross-origin + // no-cors request, requested with credentials. + assert_equals(await receive(this_token_1), "ServiceWorker: Proxying"); + assert_equals(await receive(this_token_1), "ServiceWorker: Fetch success"); + + // The COEP:credentialless Document is allowed by CORP to get it. + assert_equals(await receive(this_token_2), "Document: Fetch success"); + + // test.add_cleanup doesn't allow waiting for a promise. Unregistering a + // ServiceWorker is an asynchronous operation. It might not be completed on + // time for the next test. Do it here for extra flakiness safety. + await sw_registration.unregister() +}, "COEP:credentialless ServiceWorker"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-none-proxy.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-none-proxy.https.window.js new file mode 100644 index 0000000000..21969bb7ed --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker-coep-none-proxy.https.window.js @@ -0,0 +1,87 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + +promise_test(async test => { + const this_token_1 = token(); + const this_token_2 = token(); + + // Register a COEP:none ServiceWorker. + const sw_token = token(); + const sw_url = executor_service_worker_path + coep_none + `&uuid=${sw_token}`; + // Executors should be controlled by the service worker. + const scope = executor_path; + const sw_registration = + await service_worker_unregister_and_register(test, sw_url, scope); + test.add_cleanup(() => sw_registration.unregister()); + await wait_for_state(test, sw_registration.installing, 'activated'); + + // Configure the ServiceWorker to proxy the fetch requests. Wait for the + // worker to be installed and activated. + send(sw_token, ` + fetchHandler = event => { + if (!event.request.url.includes("/proxied")) + return; + + send("${this_token_1}", "ServiceWorker: Proxying"); + + // Response with a cross-origin no-cors resource. + const url = "${cross_origin}" + "/common/blank.html}"; + + event.respondWith(new Promise(async resolve => { + try { + let response = await fetch(url, { + mode: "no-cors", + credentials: "include" + }); + send("${this_token_1}", "ServiceWorker: Fetch success"); + resolve(response); + } catch (error) { + send("${this_token_1}", "ServiceWorker: Fetch failure"); + resolve(new Response("", {status: 400})); + } + })); + } + + await clients.claim(); + + send("${this_token_1}", serviceWorker.state); + `) + assert_equals(await receive(this_token_1), "activated"); + + // Create a COEP:credentialless document. + const document_token = environments["document"](coep_credentialless)[0]; + + // The document fetches a same-origin no-cors resource. The requests needs to + // be same-origin to be handled by the ServiceWorker. + send(document_token, ` + try { + const response = await fetch("/proxied", { + mode: "no-cors", + credentials: "include" + }); + + send("${this_token_2}", "Document: Fetch success"); + } catch (error) { + send("${this_token_2}", "Document: Fetch error"); + } + `); + + // The COEP:unsafe-none ServiceWorker is able to handle the cross-origin + // no-cors request, requested with credentials. + assert_equals(await receive(this_token_1), "ServiceWorker: Proxying"); + assert_equals(await receive(this_token_1), "ServiceWorker: Fetch success"); + + // However, the COEP:credentialless Document is disallowed by CORP to get it. + assert_equals(await receive(this_token_2), "Document: Fetch error"); + + // test.add_cleanup doesn't allow waiting for a promise. Unregistering a + // ServiceWorker is an asynchronous operation. It might not be completed on + // time for the next test. Do it here for extra flakiness safety. + await sw_registration.unregister() +}, "COEP:unsafe-none ServiceWorker"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker.https.window.js new file mode 100644 index 0000000000..4fc0061c57 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/service-worker.https.window.js @@ -0,0 +1,113 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js +// META: script=./resources/common.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "credentialless_service_worker"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +promise_test(async t => { + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One iframe with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = document.createElement("iframe"); + w_control.src = w_control_url; + document.body.appendChild(w_control); + + // One iframe with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = document.createElement("iframe"); + w_credentialless.src = w_credentialless_url; + document.body.appendChild(w_credentialless); + + const serviceWorkerTest = function( + description, origin, coep_for_worker, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test(async test => { + // Create workers for both window. + const control_worker_token = token(); + const credentialless_worker_token = token(); + + const w_control_worker_src = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${control_worker_token}`; + const w_control_worker_reg = + await service_worker_unregister_and_register( + test, w_control_worker_src, w_control_url); + + const w_credentialless_worker_src = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${credentialless_worker_token}`; + const w_credentialless_worker_reg = + await service_worker_unregister_and_register( + test, w_credentialless_worker_src, w_credentialless_url); + + // Fetch resources from the workers. + const control_request_token = token(); + const credentialless_request_token = token(); + const control_request_url = showRequestHeaders(origin, control_request_token); + const credentialless_request_url = showRequestHeaders(origin, credentialless_request_token); + send(control_worker_token, ` + fetch("${control_request_url}", { + mode: 'no-cors', + credentials: 'include' + }) + `); + send(credentialless_worker_token, ` + fetch("${credentialless_request_url}", { + mode: 'no-cors', + credentials: 'include' + }) + `); + + // Retrieve the resource request headers. + const headers_control = JSON.parse(await receive(control_request_token)); + const headers_credentialless = JSON.parse(await receive(credentialless_request_token)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + + w_control_worker_reg.unregister(); + w_credentialless_worker_reg.unregister(); + }, `fetch ${description}`) + }; + + serviceWorkerTest("same-origin", + same_origin, coep_none, + cookie_same_origin, + cookie_same_origin); + + serviceWorkerTest("same-origin + credentialless worker", + same_origin, coep_credentialless, + cookie_same_origin, + cookie_same_origin); + + serviceWorkerTest("cross-origin", + cross_origin, coep_none, + cookie_cross_origin, + cookie_cross_origin); + + serviceWorkerTest("cross-origin + credentialless worker", + cross_origin, coep_credentialless, + undefined, + undefined); +}) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/shared-worker.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/shared-worker.https.window.js new file mode 100644 index 0000000000..0bfa72e2e5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/shared-worker.https.window.js @@ -0,0 +1,119 @@ +// META: timeout=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "credentialless_shared_worker"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +promise_test(async test => { + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let GetCookie = (response) => { + const headers_credentialless = JSON.parse(response); + return parseCookies(headers_credentialless)[cookie_key]; + } + + const sharedWorkerTest = function( + description, origin, coep_for_worker, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async t => { + // Create workers for both window. + const worker_token_1 = token(); + const worker_token_2 = token(); + + // Used to check for errors creating the DedicatedWorker. + const worker_error_1 = token(); + const worker_error_2 = token(); + + const w_worker_src_1 = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${worker_token_1}`; + send(w_control_token, ` + let worker = new SharedWorker("${w_worker_src_1}", {}); + worker.onerror = () => { + send("${worker_error_1}", "Worker blocked"); + } + `); + + const w_worker_src_2 = same_origin + executor_worker_path + + coep_for_worker + `&uuid=${worker_token_2}`; + send(w_credentialless_token, ` + let worker = new SharedWorker("${w_worker_src_2}", {}); + worker.onerror = () => { + send("${worker_error_2}", "Worker blocked"); + } + `); + + // Fetch resources with the workers. + const request_token_1 = token(); + const request_token_2 = token(); + const request_url_1 = showRequestHeaders(origin, request_token_1); + const request_url_2 = showRequestHeaders(origin, request_token_2); + send(worker_token_1, + `fetch("${request_url_1}", {mode: 'no-cors', credentials: 'include'})`); + send(worker_token_2, + `fetch("${request_url_2}", {mode: 'no-cors', credentials: 'include'})`); + + const response_control = await Promise.race([ + receive(worker_error_1), + receive(request_token_1).then(GetCookie) + ]); + assert_equals(response_control, + expected_cookies_control, + "coep:none => "); + + const response_credentialless = await Promise.race([ + receive(worker_error_2), + receive(request_token_2).then(GetCookie) + ]); + assert_equals(response_credentialless, + expected_cookies_credentialless, + "coep:credentialless => "); + }, `fetch ${description}`) + }; + + sharedWorkerTest("same-origin", + same_origin, coep_none, + cookie_same_origin, + cookie_same_origin); + + sharedWorkerTest("same-origin + credentialless worker", + same_origin, coep_credentialless, + cookie_same_origin, + cookie_same_origin); + + sharedWorkerTest("cross-origin", + cross_origin, coep_none, + cookie_cross_origin, + cookie_cross_origin); + + sharedWorkerTest("cross-origin + credentialless worker", + cross_origin, coep_credentialless, + undefined, + undefined); +}) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js new file mode 100644 index 0000000000..0410b48564 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js @@ -0,0 +1,53 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const cookie_key = "coep_credentialless_image"; +const cookie_same_origin = "same_origin"; +const cookie_cross_origin = "cross_origin"; + +promise_setup(async test => { + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); +}, "Setup cookies"); + +const videoTest = function(description, origin, mode, expected_cookie) { + promise_test(async test => { + const video_token = token(); + + let video = document.createElement("video"); + video.src = showRequestHeaders(origin, video_token); + video.autoplay = true; + if (mode) + video.crossOrigin = mode; + document.body.appendChild(video); + + const headers = JSON.parse(await receive(video_token)); + + assert_equals(parseCookies(headers)[cookie_key], expected_cookie); + }, `video ${description}`) +}; + +// Same-origin request always contains Cookies: +videoTest("same-origin + undefined", + same_origin, undefined, cookie_same_origin); +videoTest("same-origin + anonymous", + same_origin, 'anonymous', cookie_same_origin); +videoTest("same-origin + use-credentials", + same_origin, 'use-credentials', cookie_same_origin); + +// Cross-origin request contains cookies, only when sent in CORS mode, using +// crossOrigin = "use-credentials". +videoTest("cross-origin + undefined", + cross_origin, '', undefined); +videoTest("cross-origin + anonymous", + cross_origin, 'anonymous', undefined); +videoTest("cross-origin + use-credentials", + cross_origin, 'use-credentials', cookie_cross_origin); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js.headers new file mode 100644 index 0000000000..68fde79c91 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/video.https.window.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy:credentialless diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/websocket.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/websocket.https.window.js new file mode 100644 index 0000000000..7d0dd95d84 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/credentialless/websocket.https.window.js @@ -0,0 +1,77 @@ +// META: TIMEOUT=long +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./resources/common.js + +promise_test_parallel(async test => { + const same_origin = get_host_info().HTTPS_ORIGIN; + const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + const cookie_key = "coep_credentialless_websocket"; + const cookie_same_origin = "same_origin"; + const cookie_cross_origin = "cross_origin"; + + await Promise.all([ + setCookie(same_origin, cookie_key, cookie_same_origin + + cookie_same_site_none), + setCookie(cross_origin, cookie_key, cookie_cross_origin + + cookie_same_site_none), + ]); + + // One window with COEP:none. (control) + const w_control_token = token(); + const w_control_url = same_origin + executor_path + + coep_none + `&uuid=${w_control_token}` + const w_control = window.open(w_control_url); + add_completion_callback(() => w_control.close()); + + // One window with COEP:credentialless. (experiment) + const w_credentialless_token = token(); + const w_credentialless_url = same_origin + executor_path + + coep_credentialless + `&uuid=${w_credentialless_token}`; + const w_credentialless = window.open(w_credentialless_url); + add_completion_callback(() => w_credentialless.close()); + + let WebSocketTest = function( + description, origin, + expected_cookies_control, + expected_cookies_credentialless) + { + promise_test_parallel(async test => { + const token_1 = token(); + const token_2 = token(); + + const origin_for_websocket = origin.replace("https", "wss"); + + send(w_control_token, ` + var ws = new WebSocket("${showRequestHeaders(origin_for_websocket, token_1)}"); + `); + + send(w_credentialless_token, ` + var ws = new WebSocket("${showRequestHeaders(origin_for_websocket, token_2)}"); + `); + + const headers_control = JSON.parse(await receive(token_1)); + const headers_credentialless = JSON.parse(await receive(token_2)); + + assert_equals(parseCookies(headers_control)[cookie_key], + expected_cookies_control, + "coep:none => "); + assert_equals(parseCookies(headers_credentialless)[cookie_key], + expected_cookies_credentialless, + "coep:credentialless => "); + }, `WebSocket ${description}`) + }; + + // Same-origin request always contains Cookies: + WebSocketTest("same-origin", + same_origin, + cookie_same_origin, + cookie_same_origin); + + // Cross-origin request also always contains Cookies: + WebSocketTest("cross-origin", + cross_origin, + cookie_cross_origin, + cookie_cross_origin); +}, "Main"); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js new file mode 100644 index 0000000000..9190303206 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js @@ -0,0 +1,74 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./credentialless/resources/common.js +// META: script=./resources/common.js + +const cors_coep_headers = coep_require_corp + corp_cross_origin; +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; + +const newIframe = async ( + test, + parent_origin, + parent_headers, + child_origin, + child_headers +) => { + const [future_child, future_error] = + await createIsolatedFrame(parent_origin, parent_headers); + future_error.then(test.unreached_func('cannot create isolated iframe.')); + + const child = await future_child; + add_completion_callback(() => child.remove()); + + const grand_child_token = token(); + const grand_child = child.contentDocument.createElement('iframe'); + grand_child.src = child_origin + executor_path + child_headers + + `&uuid=${grand_child_token}`; + child.contentDocument.body.appendChild(grand_child); + add_completion_callback(() => grand_child.remove()); + + return grand_child_token; +}; + +const childFrameIsCrossOriginIsolated = async ( + test, + child_origin, + parent_permission_coi +) => { + let parent_headers = cors_coep_headers; + const child_headers = cors_coep_headers; + if (parent_permission_coi !== undefined) { + // Escape right parenthesis in WPT pipe: + parent_permission_coi = parent_permission_coi.replace(')', '\\)'); + parent_headers += `|header(permissions-policy,` + + `cross-origin-isolated=${parent_permission_coi})`; + } + const parent_origin = same_origin; + const iframe = await newIframe( + test, + parent_origin, + parent_headers, + child_origin, + child_headers); + return IsCrossOriginIsolated(iframe); +} + +const generate_iframe_test = async (origin, isolation, expect_coi) => { + promise_test_parallel(async (test) => { + const isCrossOriginIsolated = + await childFrameIsCrossOriginIsolated(test, origin, isolation); + assert_equals(isCrossOriginIsolated, expect_coi) + }, `iframe (origin: ${origin}) cross origin isolated (${isolation}) ` + + `permission test`); +} + +generate_iframe_test(same_origin, undefined, true); +generate_iframe_test(same_origin, '*', true); +generate_iframe_test(same_origin, 'self', true); +generate_iframe_test(same_origin, '()', false); +generate_iframe_test(cross_origin, undefined, false); +generate_iframe_test(cross_origin, '*', false); +generate_iframe_test(cross_origin, 'self', false); +generate_iframe_test(cross_origin, '()', false);
\ No newline at end of file diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js.headers new file mode 100644 index 0000000000..3b7825def9 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-iframe.https.window.js.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin
\ No newline at end of file diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js new file mode 100644 index 0000000000..d9431cdb50 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js @@ -0,0 +1,170 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=./credentialless/resources/common.js +// META: script=./resources/common.js + +const cors_coep_headers = coep_require_corp + corp_cross_origin; +const same_origin = get_host_info().HTTPS_ORIGIN; +const cross_origin = get_host_info().HTTPS_REMOTE_ORIGIN; +const dedicatedWorkerPostMessage = ` + self.addEventListener('message', (e) => { + e.data.port.postMessage(self.crossOriginIsolated); + }); +`; + +const postMessageIsWorkerCrossOriginIsolated = async ( + test, + frame, + worker_url +) => { + const worker = new frame.contentWindow.Worker(worker_url); + const mc = new MessageChannel(); + worker.postMessage({port: mc.port2}, [mc.port2]); + worker.onerror = test.unreached_func('cannot create dedicated worker'); + return (await new Promise(r => mc.port1.onmessage = r)).data; +} + +const isDataDedicatedWorkerCrossOriginIsolated = async ( + test, + parent_headers +) => { + const [future_child, future_error] = + await createIsolatedFrame('', parent_headers); + future_error.then(test.unreached_func('cannot create isolated iframe')); + + const child = await future_child; + add_completion_callback(() => child.remove()); + + const worker_url = + `data:application/javascript;base64,${btoa(dedicatedWorkerPostMessage)}`; + return postMessageIsWorkerCrossOriginIsolated(test, child, worker_url); +} + +const isBlobURLDedicatedWorkerCrossOriginIsolated = async( + test, + parent_headers +) => { + const [future_child, future_error] = + await createIsolatedFrame("", parent_headers); + future_error.then(test.unreached_func('cannot create isolated iframe')); + + const child = await future_child; + add_completion_callback(() => child.remove()); + + const blob = + new Blob([dedicatedWorkerPostMessage], {type: 'text/plaintext'}); + const workerURL = URL.createObjectURL(blob); + return postMessageIsWorkerCrossOriginIsolated(test, child, workerURL) +} + +const isHTTPSDedicatedWorkerCrossOriginIsolated = async( + test, + parent_headers +) => { + const [future_child, future_error] = + await createIsolatedFrame("", parent_headers); + future_error.then(test.unreached_func('cannot create isolated iframe')); + + const child = await future_child; + add_completion_callback(() => child.remove()); + + const worker_token = token(); + const workerURL = + `${executor_worker_path}${cors_coep_headers}&uuid=${worker_token}`; + const worker = new child.contentWindow.Worker(workerURL); + return IsCrossOriginIsolated(worker_token); +} + +const sharedWorkerIsCrossOriginIsolated = async( + test, + withCoopCoep +) => { + const [worker, future_error] = + environments.shared_worker(withCoopCoep ? cors_coep_headers : ""); + future_error.then(test.unreached_func('cannot create shared worker.')); + return IsCrossOriginIsolated(worker); +} + +const serviceWorkerIsCrossOriginIsolated = async( + test, + withCoopCoep +) => { + const [worker, future_error] = + environments.service_worker(withCoopCoep ? cors_coep_headers : ""); + future_error.then(test.unreached_func('cannot create service worker.')); + return IsCrossOriginIsolated(worker); +} + +const dedicatedWorkerIsCrossOriginIsolated = async ( + test, + scheme, + parent_permission_coi +) => { + let parent_headers = cors_coep_headers; + if (parent_permission_coi !== undefined) { + // Escape right parenthesis in WPT cors_coep_headers: + parent_permission_coi = parent_permission_coi.replace(')', '\\)'); + parent_headers += `|header(permissions-policy,` + + `cross-origin-isolated=${parent_permission_coi})`; + } + switch (scheme) { + case 'https': + return isHTTPSDedicatedWorkerCrossOriginIsolated(test, parent_headers); + case 'data': + return isDataDedicatedWorkerCrossOriginIsolated(test, parent_headers); + case 'blob': + return isBlobURLDedicatedWorkerCrossOriginIsolated(test, parent_headers); + default: + assert_unreached("wrong scheme for dedicated worker test."); + } +} + +const generate_shared_worker_test = async (withCoopCoep, expected) => { + promise_test_parallel(async (test) => { + const isCrossOriginIsolated = + await sharedWorkerIsCrossOriginIsolated(test, withCoopCoep); + assert_equals(isCrossOriginIsolated, expected) + }, `shared_worker (withCoopCoep: ${withCoopCoep}) ` + + `cross origin isolated permission test`); +} + +const generate_dedicated_worker_test = async ( + scheme, + parent_permission_coi, + expected +) => { + promise_test_parallel(async (test) => { + const isCrossOriginIsolated = + await dedicatedWorkerIsCrossOriginIsolated(test, scheme, parent_permission_coi); + assert_equals(isCrossOriginIsolated, expected) + }, `dedicated_worker (scheme: ${scheme}) cross origin ` + + `isolated (${parent_permission_coi}) permission test`); +} + +const generate_service_worker_test = async (withCoopCoep, expected) => { + promise_test_parallel(async (test) => { + const isCrossOriginIsolated = + await serviceWorkerIsCrossOriginIsolated(test, withCoopCoep); + assert_equals(isCrossOriginIsolated, expected) + }, `service_worker (withCoopCoep: ${withCoopCoep}) ` + + `cross origin isolated permission test`); +} + +generate_shared_worker_test(false, false); +generate_shared_worker_test(true, true); + +generate_dedicated_worker_test('https', undefined, true); +generate_dedicated_worker_test('https', '*', true); +generate_dedicated_worker_test('https', 'self', true); +generate_dedicated_worker_test('https', '()', false); +generate_dedicated_worker_test('data', undefined, false); +generate_dedicated_worker_test('data', '*', false); +generate_dedicated_worker_test('data', 'self', false); +generate_dedicated_worker_test('blob', undefined, true); +generate_dedicated_worker_test('blob', '*', true); +generate_dedicated_worker_test('blob', 'self', true); +generate_dedicated_worker_test('blob', '()', false); + +generate_service_worker_test(false, false); +generate_service_worker_test(true, true);
\ No newline at end of file diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js.headers new file mode 100644 index 0000000000..3b7825def9 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/cross-origin-isolated-permission-worker.https.window.js.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin
\ No newline at end of file diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html new file mode 100644 index 0000000000..f2878dfc54 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html @@ -0,0 +1,20 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/script-factory.js"></script> +<div id=log></div> +<script> +async_test(t => { + window.addEventListener("message", t.step_func_done(({ data }) => { + assert_equals(data.id, ""); + assert_equals(data.origin, "null"); + assert_false(data.sameOriginNoCORPSuccess); // This is effectively a no-op for this test + assert_true(data.crossOriginNoCORPFailure, "Cross-origin without CORP did not fail"); + })); + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = `data:text/html,<script>${encodeURIComponent(createScript("null", window.origin))}<\/script>`; + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy and data: URLs"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/data.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker-cache-storage.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker-cache-storage.https.html new file mode 100644 index 0000000000..2c97e6f875 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker-cache-storage.https.html @@ -0,0 +1,128 @@ +<!doctype html> +<html> +<title> Check enforcement of COEP in a DedicatedWorker using CacheStorage. </title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// See also: ./shared-worker-cache-storage.https.html + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +const iframe_path = "./resources/iframe.html?pipe="; +const dedicated_worker_path = "./universal-worker.js?pipe="; +const ressource_path = "/images/blue.png?pipe="; + +const coep_header= { + "coep-none" : "", + "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)", +} + +const corp_header = { + "corp-undefined": "", + "corp-cross-origin": "|header(Cross-Origin-Resource-Policy,cross-origin)", +} + +// Check enforcement of COEP in a DedicatedWorker using CacheStorage. +// +// 1) Fetch a response from a document with COEP:none. Store it in the +// CacheStorage. The response is cross-origin without any CORS header. +// 2) From an iframe, start a DedicatedWorker and try to retrieve the response +// from the CacheStorage. +// +// Test parameters: +// - |iframe_coep| the COEP header of the iframe's document response +// - |worker_coep| the COEP header of the DedicatedWorker's script response. +// - |response_corp| the CORP header of the response. +// +// Test expectations: +// |result| +// - "success" when the worker is able to fetch the response from the +// CacheStorage, +// - "failure" when the worker is not able to fetch the response from the +// CacheStorage, and +// - "error" when it is unable to create a worker. +// https://mikewest.github.io/corpp/#initialize-embedder-policy-for-global +function check( + // Test parameters: + iframe_coep, + worker_coep, + response_corp, + + // Test expectations: + result) { + + promise_test(async (t) => { + // 1) Fetch a response from a document with COEP:none. Store it in the + // CacheStorage. The response is cross-origin without any CORS header. + const resource_path = ressource_path + corp_header[response_corp]; + const resource_url = remote(resource_path); + const fetch_request = new Request(resource_url, {mode: 'no-cors'}); + const cache = await caches.open('v1'); + const fetch_response = await fetch(fetch_request); + await cache.put(fetch_request, fetch_response); + + // 2) From an iframe, start a DedicatedWorker and try to retrieve the + // response from the CacheStorage. + const worker_url = dedicated_worker_path + coep_header[worker_coep]; + const worker_eval = ` + (async function() { + const cache = await caches.open('v1'); + const request = new Request('${resource_url}', { + mode: 'no-cors' + }); + try { + const response = await cache.match(request); + postMessage('success'); + } catch(error) { + postMessage('failure'); + } + })() + `; + + const iframe_url = iframe_path + coep_header[iframe_coep]; + const iframe_eval = ` + (async function() { + const w = new Worker('${worker_url}'); + const worker_response = new Promise(resolve => w.onmessage = resolve); + w.onerror = () => parent.postMessage('error'); + w.postMessage(\`${worker_eval}\`); + const response = await worker_response; + parent.postMessage(response.data); + })(); + `; + + const iframe = document.createElement("iframe"); + t.add_cleanup(() => iframe.remove()); + iframe.src = iframe_url; + const iframe_loaded = new Promise(resolve => iframe.onload = resolve); + document.body.appendChild(iframe); + await iframe_loaded; + + const iframe_response = new Promise(resolve => { + window.addEventListener("message", resolve); + }) + iframe.contentWindow.postMessage(iframe_eval); + + const {data} = await iframe_response; + assert_equals(data, result); + }, `${iframe_coep} ${worker_coep} ${response_corp}`) +} + +// ----------------------------------------------------------------------------- +// iframe_coep , worker_coep , response_corp , loaded +// ----------------------------------------------------------------------------- +check("coep-none" , "coep-none" , "corp-cross-origin" , "success"); +check("coep-none" , "coep-none" , "corp-undefined" , "success"); +check("coep-none" , "coep-require-corp" , "corp-cross-origin" , "success"); +check("coep-none" , "coep-require-corp" , "corp-undefined" , "failure"); +check("coep-require-corp" , "coep-none" , "corp-cross-origin" , "error"); +check("coep-require-corp" , "coep-none" , "corp-undefined" , "error"); +check("coep-require-corp" , "coep-require-corp" , "corp-cross-origin" , "success"); +check("coep-require-corp" , "coep-require-corp" , "corp-undefined" , "failure"); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker.https.html new file mode 100644 index 0000000000..1ba624181c --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/dedicated-worker.https.html @@ -0,0 +1,214 @@ +<!doctype html> +<title>COEP and dedicated worker</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/worker-support.js"></script> +<body> +<script> + +const targetUrl = resolveUrl("/common/blank.html", { + host: get_host_info().REMOTE_HOST, +}).href; + +function workerUrl(options) { + return resolveUrl("resources/dedicated-worker.js", options); +} + +async function createWorker(t, url, options) { + const { ownerCoep, workerOptions } = options || {}; + + const frameUrl = resolveUrl("/common/blank.html", { + coep: ownerCoep, + }); + const frame = await withIframe(t, frameUrl); + + return new frame.contentWindow.Worker(url, workerOptions); +} + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl()); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'LOADED'); +}, 'COEP: none worker in COEP: none frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl(), { + ownerCoep: "require-corp", + }); + await new Promise(resolve => { + worker.onerror = resolve; + }); +}, 'COEP: none worker in COEP: require-corp frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl({ coep: "require-corp" })); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, 'COEP: require-corp worker in COEP: none frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl({ coep: "require-corp" }), { + ownerCoep: "require-corp", + }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, 'COEP: require-corp worker in COEP: require-corp frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl(), { + workerOptions: { type: 'module' }, + }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'LOADED'); +}, 'COEP: none module worker in COEP: none frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl(), { + ownerCoep: "require-corp", + workerOptions: { type: 'module' }, + }); + await new Promise(resolve => { + worker.onerror = resolve; + }); +}, 'COEP: none module worker in COEP: require-corp frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl({ coep: "require-corp" }), { + workerOptions: { type: 'module' }, + }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, 'COEP: require-corp module worker in COEP: none frame'); + +promise_test(async (t) => { + const worker = await createWorker(t, workerUrl({ coep: "require-corp" }), { + ownerCoep: "require-corp", + workerOptions: { type: 'module' }, + }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, 'COEP: require-corp module worker in COEP: require-corp frame'); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "blob", + }); + + const worker = await createWorker(t, url, { ownerCoep: "require-corp" }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, "COEP: worker inherits COEP for blob URL."); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "blob", + }); + + const worker = await createWorker(t, url); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, "COEP: worker inherits COEP from blob URL creator, not owner."); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "data", + }); + + const worker = await createWorker(t, url, { ownerCoep: "require-corp" }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, "COEP: worker inherits COEP for data URL."); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "data", + }); + + const worker = await createWorker(t, url); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'LOADED'); +}, "COEP: worker inherits COEP from owner, not data URL creator."); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "filesystem", + }); + + const worker = await createWorker(t, url, { ownerCoep: "require-corp" }); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, "COEP: worker inherits COEP for filesystem URL."); + +promise_test(async (t) => { + const url = await createLocalUrl(t, { + url: workerUrl(), + creatorCoep: "require-corp", + scheme: "filesystem", + }); + + const worker = await createWorker(t, url); + worker.onerror = t.unreached_func('Worker.onerror should not be called'); + + worker.postMessage(targetUrl); + + const result = await waitForMessage(worker); + assert_equals(result.data, 'FAILED'); +}, "COEP: worker inherits COEP from filesystem URL creator, not owner."); + +</script> +</body> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/header-parsing.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/header-parsing.https.html new file mode 100644 index 0000000000..7a25eed51f --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/header-parsing.https.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<html> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +<body> +<script> +'use strict'; +function createIframe(t, values) { + const parent = document.createElement('iframe'); + const child = document.createElement('iframe'); + const params = values.map((value) => { + const percentEncodedValue = typeof value === "object" ? value.percentEncoded : encodeURIComponent(value); + return `value=${percentEncodedValue}`; + }); + parent.setAttribute('src', `resources/empty-coep.py?${params.join("&")}`); + document.body.appendChild(parent); + t.add_cleanup(() => parent.remove()); + + return new Promise((resolve, reject) => { + parent.onload = resolve; + parent.onerror = () => + reject(new Error(`failed to load from ${parent.src}`)); + }) + .then(() => { + child.setAttribute('src', '/common/blank.html'); + parent.contentDocument.body.appendChild(child); + return new Promise((resolve) => { + child.onload = resolve; + child.onerror = () => + reject(new Error(`failed to load from ${child.src}`)); + }); + }) + .then(() => child); +} + +[ + [], + [''], + ['jibberish'], + [{ percentEncoded: 'require%FFcorp' }], // non-ASCII byte + ['require-corp;'], + ['\u000brequire-corp\u000b'], // vertical tab + ['\u000crequire-corp\u000c'], // form feed + ['\u000drequire-corp\u000d'], // carriage return + ['Require-corp'], + ['"require-corp"'], // HTTP structured header "string" item + [':cmVxdWlyZS1jb3Jw:'], // HTTP structured header "byte sequence" item + ['require-corp;\tfoo=bar'], + ['require-corp require-corp'], + ['require-corp,require-corp'], + ['require-corp', 'require-corp'], + ['', 'require-corp'], + ['require-corp', ''], +].forEach((values) => { + promise_test((t) => { + return createIframe(t, values) + .then((child) => { + assert_not_equals(child.contentDocument, null); + }); + }, 'navigation allowed for ' + JSON.stringify(values)); +}); + +[ + ['require-corp'], + [' require-corp '], + ['\trequire-corp\t'], // leading and trailing OWS is not part of the field-value per HTTP + [' \trequire-corp'], + ['require-corp\t '], + ['require-corp; foo=bar'], + ['require-corp;require-corp'], + ['require-corp; report-to="data:', '"'], // `require-corp; report-to="data:, "` + +].forEach((values) => { + promise_test((t) => { + return createIframe(t, values) + .then((child) => { + assert_equals(child.contentDocument, null); + }); + }, 'navigation blocked for ' + JSON.stringify(values)); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/iframe-history-none-require-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/iframe-history-none-require-corp.https.html new file mode 100644 index 0000000000..0e7ef8108b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/iframe-history-none-require-corp.https.html @@ -0,0 +1,54 @@ +<meta name="timeout" content="long"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/common/utils.js></script +<script src="/common/get-host-info.sub.js"></script> +<script> + +promise_test(async test => { + // TODO(arthursonzogni): Consider switching toward another message passing + // API like: + // /common/dispatcher/dispatcher.js + const bc = new BroadcastChannel(token()); + const futureMessage = () => { + return new Promise(resolve => { + bc.onmessage = event => resolve(event.data); + }); + }; + + const prefix = document.URL.substr(0, document.URL.lastIndexOf('/')) + const attribute = `?channelName=${bc.name}`; + const url_coep_none = + prefix + "/resources/navigate-none.sub.html" + attribute; + const url_coep_require_corp = + prefix + "/resources/navigate-require-corp.sub.html" + attribute; + + const w = window.open(); + test.add_cleanup(() => w.close()); + + // Navigate to COEP:unsafe-none. + w.location.href = url_coep_none; + assert_equals(await futureMessage(), "loaded"); + assert_equals(w.location.href, url_coep_none); + + // For unknown reasons so far. Waiting in between the different navigations + // avoids flakes. + await new Promise(resolve => test.step_timeout(resolve, 1000)); + + // Navigate to COEP:require-corp. + w.location.href = url_coep_require_corp; + assert_equals(await futureMessage(), "loaded"); + assert_equals(w.location.href, url_coep_require_corp); + + // For unknown reasons so far. Waiting in between the different navigations + // avoids flakes. + await new Promise(resolve => test.step_timeout(resolve, 1000)); + + // Navigate back to COEP:unsafe-none, using the history API. + // Note: `url_coep_none` already take the BFCache into account. + w.history.back(); + assert_equals(await futureMessage(), "loaded"); + assert_equals(w.location.href, url_coep_none); +}, `"none" top-level: navigating a frame back from "require-corp" should succeed`); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html new file mode 100644 index 0000000000..60edf00312 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html @@ -0,0 +1,21 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/script-factory.js"></script> +<div id=log></div> +<script> +async_test(t => { + window.addEventListener("message", t.step_func_done(({ data }) => { + assert_equals(data.id, ""); + assert_equals(data.origin, window.origin); + assert_true(data.sameOriginNoCORPSuccess); + assert_true(data.crossOriginNoCORPFailure, "Cross-origin without CORP did not fail"); + })); + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = `javascript:${encodeURIComponent(createScript(window.origin, get_host_info().HTTPS_NOTSAMESITE_ORIGIN))}`; + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy and javascript: URLs"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/javascript.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/meta-http-equiv.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/meta-http-equiv.https.html new file mode 100644 index 0000000000..d35df3135a --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/meta-http-equiv.https.html @@ -0,0 +1,20 @@ +<!doctype html> +<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp"><!-- should not be supported --> +<title>Cross-Origin-Embedder-Policy in <meta http-equiv></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "/common/blank.html"; + document.body.append(frame); + assert_equals(frame.contentDocument.URL, "about:blank"); + assert_equals(frame.contentDocument.body.localName, "body"); + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentDocument.URL, `${location.protocol}//${location.host}/common/blank.html`); + assert_equals(frame.contentDocument.body.localName, "body"); + }); +}, `<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp"> top-level: navigating a frame to "none" should not fail`); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html new file mode 100644 index 0000000000..e6261f8388 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html @@ -0,0 +1,3 @@ +<!doctype html> +<meta charset=utf-8> +<title>Current page</title> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/current.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/worker.js new file mode 100644 index 0000000000..44103842a4 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/current/worker.js @@ -0,0 +1 @@ +postMessage('current'); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html new file mode 100644 index 0000000000..d8bd1ae2c0 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Incumbent page</title> + +<iframe src="../current/current.html" id="c"></iframe> + +<script> + const current = document.querySelector("#c").contentWindow; + + window.hello = () => { + const worker = new current.Worker('worker.js'); + worker.onmessage = e => { parent.postMessage(e.data, '*'); } + }; +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/incumbent.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/worker.js new file mode 100644 index 0000000000..03f02a8690 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/incumbent/worker.js @@ -0,0 +1 @@ +postMessage('incumbent'); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/worker.js new file mode 100644 index 0000000000..fcc521e313 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/worker.js @@ -0,0 +1 @@ +postMessage('entry'); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html new file mode 100644 index 0000000000..e1f16a61e1 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Multiple globals for Worker constructor: COEP reports</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- This is the entry global --> + +<iframe src="incumbent/incumbent.html"></iframe> +<button onclick="" id="button">Hello</button> + +<script> +async function observeReports(global) { + const reports = []; + const observer = new global.ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + }); + observer.observe(); + + // Wait 5000ms for reports to settle. + await new Promise(r => step_timeout(r, 5000)); + return reports; +} + +async_test((t) => { + onload = t.step_func(() => { + Promise.all([ + observeReports(window), + observeReports(frames[0]), + observeReports(frames[0].frames[0]) + ]).then(t.step_func_done(([entry, incumbent, current]) => { + assert_equals(entry.length, 0); + assert_equals(incumbent.length, 0); + assert_equals(current.length, 1); + const report = current[0]; + assert_equals(report.type, 'coep'); + assert_equals(report.url, new URL('current/current.html', location.href).href); + assert_equals(report.body.type, 'worker initialization'); + assert_equals(report.body.blockedURL, new URL('current/worker.js', location.href).href); + assert_equals(report.body.disposition, 'enforce'); + })); + + frames[0].hello(); + }); + onmessage = t.unreached_func('worker should have been blocked by COEP'); +}); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/multi-globals/workers-coep-report.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html new file mode 100644 index 0000000000..6e1573cd64 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html @@ -0,0 +1,19 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<div id=log></div> +<script> +async_test(t => { + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + frame.src = get_host_info().HTTP_NOTSAMESITE_ORIGIN + new URL("resources/iframe.html", location).pathname; + window.onmessage = t.step_func_done(({ data }) => { + assert_equals(data, "success"); + }); + frame.onload = t.step_func(() => { + frame.contentWindow.postMessage("parent.postMessage('success', '*');", "*"); + }); +}, "COEP requires a secure context"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/no-secure-context.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html new file mode 100644 index 0000000000..7fed1fe581 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html @@ -0,0 +1,21 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> +<script> +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + let i = 0; + frame.onload = t.step_func(() => { + i++; + assert_equals(frame.contentDocument.URL, "about:blank"); + frame.src = "about:blank"; + if (i == 2) { + t.done(); + } + }); + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy and about:blank"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/non-initial-about-blank.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none-load-from-cache-storage.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-load-from-cache-storage.https.html new file mode 100644 index 0000000000..177ae8d11b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-load-from-cache-storage.https.html @@ -0,0 +1,173 @@ +<!doctype html> +<html> +<title> Retrieve resources from CacheStorage with Cross-Origin-Embedder-Policy: require-corp</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> + +/* + This document does NOT define the Cross-Origin-Embedder-Policy header. + Cross-Origin Embedder Policy Editor's draft: https://mikewest.github.io/corpp/ + + This test is retrieving same-origin and cross-origin resources from the + CacheStorage. The resources are generated from the ServiceWorker or from the + network with the header Cross-Origin-Resource-Policy being one of: + - 'same-origin' + - 'cross-origin' + - <undefined> +*/ + +promise_test(async (t) => { + const SCOPE = new URL(location.href).pathname; + const SCRIPT = + 'resources/sw-store-to-cache-storage.js?' + + `pipe=header(service-worker-allowed,${SCOPE})`; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => reg.unregister()); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +function local(path) { + return new URL(path, location.origin); +} + +// Send a message to the currently active ServiceWorker and wait for its +// response. +function executeCommandInServiceWorker(command) { + return new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', e => resolve(e.data)); + navigator.serviceWorker.controller.postMessage(command); + }); +} + +// Try loading an image from a |response|. Return a Promise resolving or +// rejecting depending on the image loading result. +const loadFailure = {name: "Image.onerror"}; +function readImageFromResponse(response) { + return new Promise((resolve, reject) => { + const img = document.createElement("img"); + img.onload = resolve.bind(this, ""); + img.onerror = reject.bind(this, loadFailure); + response.blob().then(blob => { + img.src = URL.createObjectURL(blob); + document.body.appendChild(img); + }) + }) +} + +const image_path = "/images/blue.png?pipe="; + +const corp_header = { + "":"", + "corp-undefined": "", + "corp-same-origin": "|header(Cross-Origin-Resource-Policy,same-origin)", + "corp-cross-origin": "|header(Cross-Origin-Resource-Policy,cross-origin)", +} + +const cors_header = { + "":"", + "cors-disabled": "", + "cors-enabled": "|header(Access-Control-Allow-Origin,*)", +} + +function test( + // Test parameters: + request_source, request_origin, request_mode, response_cors, response_corp, + // Test expectations: + response_stored, response_type) { + promise_test(async (t) => { + // 0. Start from an empty CacheStorage. + await caches.delete("v1"); + + // 1. Make the ServiceWorker to request the ressource and store it into the + // CacheStorage. + const path = image_path + + corp_header[response_corp] + + cors_header[response_cors]; + const url = (request_origin === "same-origin" ? local : remote)(path); + const command = { + url: url.href, + mode: request_mode, + source: request_source, + }; + + assert_equals(await executeCommandInServiceWorker(command), response_stored); + if (response_stored === "not-stored") { + return; + } + + // 2. Make this document to retrieve it from the CacheStorage. + const cache = await caches.open('v1'); + const response = await cache.match(url); + + assert_equals(response.type, response_type); + + if (request_source === "service-worker") { + assert_equals("foo", await response.text()); + return; + } + + // Opaque response are not readable. + if (response_type === "opaque") { + await promise_rejects_exactly(t, loadFailure, readImageFromResponse(response)); + return; + } + + await readImageFromResponse(response); + }, `Fetch ${request_origin} ${request_mode} ${response_cors} ${response_corp} from ${request_source} and CacheStorage.`) +} + +// Responses generated from the ServiceWorker. +{ + test("service-worker", "cross-origin", "cors", "", "", "stored", "default"); + test("service-worker", "cross-origin", "no-cors", "", "", "stored", "default"); + test("service-worker", "same-origin", "cors", "", "", "stored", "default"); + test("service-worker", "same-origin", "no-cors", "", "", "stored", "default"); +} + +// Responses generated from a same-origin server. +{ + const t = test.bind(this, "network", "same-origin"); + t("cors", "cors-disabled", "corp-cross-origin", "stored", "basic"); + t("cors", "cors-disabled", "corp-same-origin", "stored", "basic"); + t("cors", "cors-disabled", "corp-undefined", "stored", "basic"); + t("cors", "cors-enabled", "corp-cross-origin", "stored", "basic"); + t("cors", "cors-enabled", "corp-same-origin", "stored", "basic"); + t("cors", "cors-enabled", "corp-undefined", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-cross-origin", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-same-origin", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-undefined", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-cross-origin", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-same-origin", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-undefined", "stored", "basic"); +} + +// Responses generated from a cross-origin server. +{ + const t = test.bind(this, "network", "cross-origin"); + t("cors", "cors-disabled", "corp-cross-origin", "not-stored"); + t("cors", "cors-disabled", "corp-same-origin", "not-stored"); + t("cors", "cors-disabled", "corp-undefined", "not-stored"); + t("cors", "cors-enabled", "corp-cross-origin", "stored", "cors"); + t("cors", "cors-enabled", "corp-same-origin", "stored", "cors"); + t("cors", "cors-enabled", "corp-undefined", "stored", "cors"); + t("no-cors", "cors-disabled", "corp-cross-origin", "stored", "opaque"); + t("no-cors", "cors-disabled", "corp-same-origin", "not-stored"); + t("no-cors", "cors-disabled", "corp-undefined", "stored", "opaque"); + t("no-cors", "cors-enabled", "corp-cross-origin", "stored", "opaque"); + t("no-cors", "cors-enabled", "corp-same-origin", "not-stored"); + t("no-cors", "cors-enabled", "corp-undefined", "stored", "opaque"); +} + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-none.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-none.https.html new file mode 100644 index 0000000000..6d2cf0f64f --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-none.https.html @@ -0,0 +1,89 @@ +<!doctype html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const SCOPE = new URL(location.href).pathname; +const SCRIPT = + 'resources/sw.js?' + + `pipe=header(service-worker-allowed,${SCOPE})`; + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN + '/html/cross-origin-embedder-policy/'); +} + +promise_test(async (t) => { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => { + reg.unregister(); + }); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +promise_test(async (t) => { + await fetch('resources/nothing-same-origin-corp.txt', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await fetch('/common/blank.html', {mode: 'no-cors'}); +}, 'making a same-origin request for no CORP'); + +promise_test(async (t) => { + await fetch('resources/nothing-cross-origin-corp.js', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt'), {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await fetch(remote('/common/blank.html'), {mode: 'no-cors'}); +}, 'making a cross-origin request for no CORP'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt?passthrough'), + {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch(remote('/common/blank.html?passthrough'), {mode: 'no-cors'}); +}, 'making a cross-origin request for no CORP [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js?passthrough'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'cors'})); +}, 'making a cross-origin request with CORS without ACAO'); + +promise_test(async (t) => { + const URL = remote( + '/common/blank.html?pipe=header(access-control-allow-origin,*)'); + await fetch(URL, {mode: 'cors'}); +}, 'making a cross-origin request with CORS'); + +promise_test(async (t) => { + const URL = remote('/fetch/api/resources/preflight.py?allow_headers=hoge'); + await fetch(URL, {mode: 'cors', headers: {'hoge': 'fuga'}}); +}, 'making a cross-origin request with CORS-preflight'); +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html new file mode 100644 index 0000000000..dc1209c40d --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const SCOPE = new URL(location.href).pathname; +const SCRIPT = + 'resources/sw.js?' + + `pipe=header(service-worker-allowed,${SCOPE})`; + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN + '/html/cross-origin-embedder-policy/'); +} + +promise_test(async (t) => { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => { + reg.unregister(); + }); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +promise_test(async (t) => { + await fetch('resources/nothing-same-origin-corp.txt', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await fetch('/common/blank.html', {mode: 'no-cors'}); +}, 'making a same-origin request for no CORP'); + +promise_test(async (t) => { + await fetch('resources/nothing-cross-origin-corp.js', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt'), {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt?passthrough'), + {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('/common/blank.html?passthrough'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js?passthrough'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'cors'})); +}, 'making a cross-origin request with CORS without ACAO'); + +promise_test(async (t) => { + const URL = remote( + '/common/blank.html?pipe=header(access-control-allow-origin,*)'); + await fetch(URL, {mode: 'cors'}); +}, 'making a cross-origin request with CORS'); + +promise_test(async (t) => { + const URL = remote('/fetch/api/resources/preflight.py?allow_headers=hoge'); + await fetch(URL, {mode: 'cors', headers: {'hoge': 'fuga'}}); +}, 'making a cross-origin request with CORS-preflight'); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none-sw-from-require-corp.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html new file mode 100644 index 0000000000..cf9b34b4ca --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html @@ -0,0 +1,91 @@ +<meta name="timeout" content="long"> +<title>Cross-Origin-Embedder-Policy header and nested navigable resource without such header</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/common/utils.js></script> <!-- Use token() to allow running tests in parallel --> +<script src="/common/get-host-info.sub.js"></script> +<div id=log></div> +<script> + +const HOST = get_host_info(); +const BASE = new URL("resources", location).pathname; + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.onload = t.step_func_done(() => { + assert_not_equals(frame.contentDocument, null); + }); + frame.src = "/common/blank.html"; + document.body.append(frame); + assert_equals(frame.contentDocument.body.localName, "body"); +}, `"none" top-level: navigating a frame to "none" should succeed`); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + const blank = "/common/blank.html"; + let firstNavOk = false; + frame.onload = t.step_func(() => { + if (!firstNavOk) { + assert_not_equals(frame.contentDocument, null); + firstNavOk = true; + } else { + assert_not_equals(frame.contentDocument, null); + assert_equals(frame.contentWindow.location.pathname, blank); + t.done(); + } + }); + frame.src = `resources/navigate-require-corp.sub.html?to=${blank}`; + document.body.append(frame); + assert_equals(frame.contentDocument.body.localName, "body"); +}, `"none" top-level: navigating a frame from "require-corp" to "none" should succeed`); + +async_test(t => { + let pageLoaded = false; + // TODO(arthursonzogni): Consider switching toward another message passing + // API like: + // /common/dispatcher/dispatcher.js + const bc = new BroadcastChannel(token()); + let finished = false; + let doneCheck = _ => { + if (finished && pageLoaded) { + t.done(); + } + } + bc.onmessage = t.step_func((event) => { + pageLoaded = true; + let payload = event.data; + assert_equals(payload, "loaded"); + + doneCheck(); + }); + + const bc2 = new BroadcastChannel(token()); + bc2.onmessage = t.step_func((event) => { + finished = true; + let payload = event.data; + assert_equals(payload, "loaded"); + + doneCheck(); + }); + + const win = window.open(`resources/navigate-require-corp.sub.html?channelName=${bc.name}&to=navigate-none.sub.html?channelName=${bc2.name}`, "_blank", "noopener"); + assert_equals(win, null); +}, `"require-corp" top-level noopener popup: navigating to "none" should succeed`); + +async_test(t => { + const frame = document.createElement("iframe"); + const id = token(); + t.add_cleanup(() => frame.remove()); + window.addEventListener('message', t.step_func((e) => { + if (e.data === id) { + // Loaded! + t.done(); + } + })); + frame.src = `${HOST.HTTPS_NOTSAMESITE_ORIGIN}${BASE}/navigate-require-corp-same-site.sub.html?token=${id}`; + document.body.append(frame); +}, 'CORP: same-site is not checked.'); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html.headers new file mode 100644 index 0000000000..43c44cffd6 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/none.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: unknown-should-be-parsed-as-null diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js new file mode 100644 index 0000000000..f4d59955af --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js @@ -0,0 +1,2 @@ +// META: global=window,worker,sharedworker-module,serviceworker-module +test(t => assert_equals(crossOriginEmbedderPolicy, "credentialless")); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js.headers new file mode 100644 index 0000000000..32523a6978 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-credentialless.tentative.https.any.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: credentialless diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js new file mode 100644 index 0000000000..f6019c2457 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js @@ -0,0 +1,2 @@ +// META: global=window,worker,sharedworker-module,serviceworker-module +test(t => assert_equals(crossOriginEmbedderPolicy, "require-corp")); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-require-corp.tentative.https.any.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-unsafe-none.tentative.https.any.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-unsafe-none.tentative.https.any.js new file mode 100644 index 0000000000..d2890901eb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reflection-unsafe-none.tentative.https.any.js @@ -0,0 +1,2 @@ +// META: global=window,worker,sharedworker-module,serviceworker-module +test(t => assert_equals(crossOriginEmbedderPolicy, "unsafe-none")); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html new file mode 100644 index 0000000000..ff9e5b64a0 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html @@ -0,0 +1,86 @@ +<!doctype html> +<meta name="timeout" content="long"> +<title>Cross-Origin-Embedder-Policy-Report-Only header does not affect the actual behavior</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/common/utils.js></script> <!-- Use token() to allow running tests in parallel --> +<script src="/common/get-host-info.sub.js"></script> +<div id=log></div> +<script> +const HOST = get_host_info(); +const BASE = new URL("resources", location).pathname; + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.onload = t.step_func_done(() => { + assert_not_equals(frame.contentDocument, null); + }); + frame.src = "/common/blank.html"; + document.body.append(frame); + assert_equals(frame.contentDocument.body.localName, "body"); +}, `"none" top-level: navigating a frame to "none" should succeed`); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + const blank = "/common/blank.html"; + let firstNavOk = false; + frame.onload = t.step_func(() => { + if (!firstNavOk) { + assert_not_equals(frame.contentDocument, null); + firstNavOk = true; + } else { + assert_not_equals(frame.contentDocument, null); + assert_equals(frame.contentWindow.location.pathname, blank); + t.done(); + } + }); + frame.src = `resources/navigate-require-corp.sub.html?to=${blank}`; + document.body.append(frame); + assert_equals(frame.contentDocument.body.localName, "body"); +}, `"none" top-level: navigating a frame from "require-corp" to "none" should succeed`); + +async_test(t => { + const w = window.open(`resources/navigate-none.sub.html?to=navigate-require-corp.sub.html`, "window_name"); + t.add_cleanup(() => w.close()); + + w.onload = t.step_func(() => { + w.history.back(); + t.step_timeout(() => { + assert_not_equals(w.document, null); + t.done(); + }, 1500); + }); +}, `"none" top-level: navigating a frame back from "require-corp" should succeed`); + +async_test(t => { + let pageLoaded = false; + const bc = new BroadcastChannel(token()); + let finished = false; + let doneCheck = _ => { + if (finished && pageLoaded) { + t.done(); + } + } + bc.onmessage = t.step_func((event) => { + pageLoaded = true; + let payload = event.data; + assert_equals(payload, "loaded"); + + doneCheck(); + }); + + const bc2 = new BroadcastChannel(token()); + bc2.onmessage = t.step_func((event) => { + finished = true; + let payload = event.data; + assert_equals(payload, "loaded"); + + doneCheck(); + }); + + const win = window.open(`resources/navigate-require-corp.sub.html?channelName=${bc.name}&to=navigate-none.sub.html?channelName=${bc2.name}`, "_blank", "noopener"); + assert_equals(win, null); +}, `"require-corp" top-level noopener popup: navigating to "none" should succeed`); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html.headers new file mode 100644 index 0000000000..289659a41f --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/report-only-require-corp.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy-report-only: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-navigation.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-navigation.https.html new file mode 100644 index 0000000000..dea8947818 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-navigation.https.html @@ -0,0 +1,170 @@ +<!doctype html> +<html> +<meta name="timeout" content="long"> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="./credentialless/resources/common.js"></script> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const COEP = '|header(cross-origin-embedder-policy,require-corp)'; +const COEP_RO = + '|header(cross-origin-embedder-policy-report-only,require-corp)'; +const CORP_CROSS_ORIGIN = + '|header(cross-origin-resource-policy,cross-origin)'; +const CSP_FRAME_ANCESTORS_NONE = + '|header(content-security-policy,frame-ancestors \'none\')'; +const XFRAMEOPTIONS_DENY = + '|header(x-frame-options,deny)'; +const FRAME_URL = `${ORIGIN}/common/blank.html?pipe=`; +const REMOTE_FRAME_URL = `${REMOTE_ORIGIN}/common/blank.html?pipe=`; + +function checkCorpReport(report, contextUrl, blockedUrl, disposition) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'corp'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); + assert_equals(report.body.destination, 'iframe'); +} + +function checkCoepMismatchReport(report, contextUrl, blockedUrl, disposition) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'navigation'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); +} + +function loadFrame(document, url) { + return new Promise((resolve, reject) => { + const frame = document.createElement('iframe'); + frame.src = url; + frame.onload = () => resolve(frame); + frame.onerror = reject; + document.body.appendChild(frame); + }); +} + +// |parentSuffix| is a suffix for the parent frame URL. +// When |withEmptyFrame| is true, this function creates an empty frame +// between the parent and target frames. +// |targetUrl| is a URL for the target frame. +async function loadFrames(test, parentSuffix, withEmptyFrame, targetUrl) { + const frame = await loadFrame(document, FRAME_URL + parentSuffix); + test.add_cleanup(() => frame.remove()); + let parent; + if (withEmptyFrame) { + parent = frame.contentDocument.createElement('iframe'); + frame.contentDocument.body.appendChild(parent); + } else { + parent = frame; + } + // Here we don't need "await". This loading may or may not succeed, and + // we're not interested in the result. + loadFrame(parent.contentDocument, targetUrl); + + return parent; +} + +async function observeReports(global, expected_count) { + const reports = []; + const receivedEveryReports = new Promise(resolve => { + if (expected_count == 0) + resolve(); + + const observer = new global.ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + if (expected_count <= reports.length) + resolve(); + }); + observer.observe(); + + }); + + // Wait 5000 ms more to catch additionnal unexpected reports. + await receivedEveryReports; + await new Promise(r => step_timeout(r, 5000)); + return reports; +} + +// CASES is a list of test case. Each test case consists of: +// parent: the suffix of the URL of the parent frame. +// target: the suffix of the URL of the target frame. +// reports: the expectation of reports to be made. Each report is one of: +// 'CORP': CORP violation +// 'CORP-RO' CORP violation (report only) +// 'NAV': COEP mismatch between the frames. +// 'NAV-RO': COEP mismatch between the frames (report only). +const CASES = [ + { parent: '', target: '', reports: [] }, + { parent: '', target: COEP, reports: [] }, + { parent: COEP, target: COEP, reports: ['CORP'] }, + { parent: COEP, target: '', reports: ['CORP'] }, + + { parent: '', target: CORP_CROSS_ORIGIN, reports: [] }, + { parent: COEP, target: CORP_CROSS_ORIGIN, reports: ['NAV'] }, + + { parent: '', target: COEP + CORP_CROSS_ORIGIN, reports: [] }, + { parent: COEP, target: COEP + CORP_CROSS_ORIGIN, reports: [] }, + + { parent: COEP_RO, target: COEP, reports: ['CORP-RO'] }, + { parent: COEP_RO, target: '', reports: ['CORP-RO', 'NAV-RO'] }, + { parent: COEP_RO, target: CORP_CROSS_ORIGIN, reports: ['NAV-RO'] }, + { parent: COEP_RO, target: COEP + CORP_CROSS_ORIGIN, reports: [] }, + + { parent: COEP, target: COEP_RO + CORP_CROSS_ORIGIN, reports: ['NAV'] }, + + // Test ordering of CSP frame-ancestors, COEP, and X-Frame-Options + { parent: COEP, target: CORP_CROSS_ORIGIN + CSP_FRAME_ANCESTORS_NONE, reports: [] }, + { parent: COEP, target: CORP_CROSS_ORIGIN + XFRAMEOPTIONS_DENY, reports: ['NAV'] }, +]; + +for (const testcase of CASES) { + for (const withEmptyFrame of [false, true]) { + function desc(s) { + return s === '' ? '(none)' : s; + } + // These tests are very slow, so they must be run in parallel using + // async_test. + async_test(t => { + const targetUrl = REMOTE_FRAME_URL + testcase.target; + loadFrames(t, testcase.parent, withEmptyFrame, targetUrl) + .then(t.step_func(parent => { + const contextUrl = parent.src ? parent.src : 'about:blank'; + observeReports(parent.contentWindow, testcase.reports.length) + .then(t.step_func(reports => { + assert_equals(reports.length, testcase.reports.length); + for (let i = 0; i < reports.length; i += 1) { + const report = reports[i]; + switch (testcase.reports[i]) { + case 'CORP': + checkCorpReport(report, contextUrl, targetUrl, 'enforce'); + break; + case 'CORP-RO': + checkCorpReport(report, contextUrl, targetUrl, 'reporting'); + break; + case 'NAV': + checkCoepMismatchReport(report, contextUrl, targetUrl, 'enforce'); + break; + case 'NAV-RO': + checkCoepMismatchReport(report, contextUrl, targetUrl, 'reporting'); + break; + default: + assert_unreached( + 'Unexpected report expeaction: ' + testcase.reports[i]); + } + } + t.done(); + })).catch(t.step_func(e => { throw e; })); + })).catch(t.step_func(e => { throw e; })); + }, `parent: ${desc(testcase.parent)}, target: ${desc(testcase.target)}, ` + + `with empty frame: ${withEmptyFrame}`); + } +} + +</script> +</body></html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html new file mode 100644 index 0000000000..e56124a4a0 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html @@ -0,0 +1,206 @@ +<!doctype html> +<html> +<meta name="timeout" content="long"> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); +const BASE = new URL("resources", location).pathname +const FRAME_URL = `${ORIGIN}/common/blank.html` + + '?pipe=header(cross-origin-embedder-policy,require-corp)' + + `|header(cross-origin-embedder-policy-report-only,require-corp)`; +const WORKER_URL = `${ORIGIN}${BASE}/reporting-worker.js` + + '?pipe=header(cross-origin-embedder-policy,require-corp)' + + `|header(cross-origin-embedder-policy-report-only,require-corp)`; +const REPORTING_FRAME_URL = `${ORIGIN}${BASE}/reporting-empty-frame.html` + + '?pipe=header(cross-origin-embedder-policy,require-corp)' + + `|header(cross-origin-embedder-policy-report-only,require-corp)`; + +async function observeReports(global, expected_count) { + const reports = []; + const receivedEveryReports = new Promise(resolve => { + if (expected_count == 0) + resolve(); + + const observer = new global.ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + if (expected_count <= reports.length) + resolve(); + }); + observer.observe(); + + }); + + await receivedEveryReports; + // Wait 500ms more to catch additionnal unexpected reports. + await new Promise(r => step_timeout(r, 500)); + return reports; +} + +function checkReport(report, contextUrl, blockedUrl, disposition, destination) { + assert_equals(report.type, 'coep'); + assert_equals(report.url, contextUrl); + assert_equals(report.body.type, 'corp'); + assert_equals(report.body.blockedURL, blockedUrl); + assert_equals(report.body.disposition, disposition); + assert_equals(report.body.destination, destination); +} + +async function fetchInFrame(t, frameUrl, url, expected_count) { + const frame = await with_iframe(frameUrl); + t.add_cleanup(() => frame.remove()); + + const init = { mode: 'no-cors', cache: 'no-store' }; + let future_reports = observeReports(frame.contentWindow, expected_count); + await frame.contentWindow.fetch(url, init).catch(() => {}); + + return await future_reports; +} + +async function fetchInWorker(workerOrPort, url) { + const script = + `fetch('${url}', {mode: 'no-cors', cache: 'no-store'}).catch(() => {});`; + const mc = new MessageChannel(); + workerOrPort.postMessage({script, port: mc.port2}, [mc.port2]); + return (await new Promise(r => mc.port1.onmessage = r)).data; +} + +// We want to test several URLs in various environments (document, +// dedicated worker, shared worser, service worker). As expectations +// are independent of environment except for the context URLs in reports, +// we define ENVIRONMENTS and CASES to reduce the code duplication. +// +// ENVIRONMENTS is a list of dictionaries. Each dictionary consists of: +// - tag: the name of the environment +// - contextUrl: the URL of the environment settings object +// - run: an async function which generates reports +// - test: a testharness Test object +// - url: the URL for a test case (see below) +// +// CASES is a list of test cases. Each test case consists of: +// - name: the name of the test case +// - url: the URL of the test case +// - check: a function to check the results +// - reports: the generated reports +// - url: the URL of the test case +// - contextUrl: the URL of the environment settings object (see +// ENVORONMENTS) + +const ENVIRONMENTS = [{ + tag: 'document', + contextUrl: FRAME_URL, + run: async (test, url, expected_count) => { + return await fetchInFrame(test, FRAME_URL, url, expected_count); + }, +}, { + tag: 'dedicated worker', + contextUrl: WORKER_URL, + run: async (test, url, expected_count) => { + const worker = new Worker(WORKER_URL); + worker.addEventListener('error', test.unreached_func('Worker.onerror')); + test.add_cleanup(() => worker.terminate()); + return await fetchInWorker(worker, url); + }, +}, { + tag: 'shared worker', + contextUrl: WORKER_URL, + run: async (test, url, expected_count) => { + const worker = new SharedWorker(WORKER_URL); + worker.addEventListener('error', test.unreached_func('Worker.onerror')); + return await fetchInWorker(worker.port, url); + }, +}, { + tag: 'service worker', + contextUrl: WORKER_URL, + run: async (test, url, expected_count) => { + // As we don't want the service worker to control any page, generate a + // one-time scope. + const SCOPE = new URL(`resources/${token()}.html`, location).pathname; + const reg = + await service_worker_unregister_and_register(test, WORKER_URL, SCOPE); + test.add_cleanup(() => reg.unregister()); + const worker = reg.installing || reg.waiting || reg.active; + worker.addEventListener('error', test.unreached_func('Worker.onerror')); + return await fetchInWorker(worker, url); + }, +}, { + tag: 'between service worker and page', + contextUrl: REPORTING_FRAME_URL, + run: async (test, url, expected_count) => { + // Here we use a Service Worker without COEP. + const WORKER_URL = `${ORIGIN}${BASE}/sw.js`; + const reg = await service_worker_unregister_and_register( + test, WORKER_URL, REPORTING_FRAME_URL); + test.add_cleanup(() => reg.unregister()); + const worker = reg.installing || reg.waiting || reg.active; + worker.addEventListener('error', test.unreached_func('Worker.onerror')); + return await fetchInFrame( + test, REPORTING_FRAME_URL, url, expected_count); + }, +}]; + +const CASES = [{ + name: 'same-origin', + url: '/common/text-plain.txt', + expected_count: 0, + check: (reports, url, contextUrl) => {} +}, { + name: 'blocked by CORP: same-origin', + url: `${REMOTE_ORIGIN}${BASE}/nothing-same-origin-corp.txt`, + expected_count: 0, + check: (reports, url, contextUrl) => {} +}, { + name: 'blocked due to COEP', + url: `${REMOTE_ORIGIN}/common/text-plain.txt`, + expected_count: 2, + check: (reports, contextUrl, url) => { + checkReport(reports[0], contextUrl, url, 'reporting', ''); + checkReport(reports[1], contextUrl, url, 'enforce', ''); + } +}, { + name: 'blocked during redirect', + url: `${ORIGIN}/common/redirect.py?location=` + + encodeURIComponent(`${REMOTE_ORIGIN}/common/text-plain.txt`), + expected_count: 2, + check: (reports, contextUrl, url) => { + checkReport(reports[0], contextUrl, url, 'reporting', ''); + checkReport(reports[1], contextUrl, url, 'enforce', ''); + }, +}]; + +for (const env of ENVIRONMENTS) { + for (const testcase of CASES) { + promise_test(async (t) => { + const reports = await env.run( + t, testcase.url, testcase.expected_count); + + assert_equals(reports.length, testcase.expected_count); + testcase.check(reports, env.contextUrl, testcase.url); + }, `[${env.tag}] ${testcase.name}`); + } +} + +// A test for a non-empty destination. +promise_test(async (t) => { + const frame = await with_iframe(FRAME_URL); + t.add_cleanup(() => frame.remove()); + + const url = `${REMOTE_ORIGIN}/common/utils.js`; + const script = frame.contentDocument.createElement('script'); + script.src = url; + const future_reports = observeReports(frame.contentWindow, 2); + frame.contentDocument.body.appendChild(script); + + const reports = await future_reports; + assert_equals(reports.length, 2); + checkReport(reports[0], FRAME_URL, url, 'reporting', 'script'); + checkReport(reports[1], FRAME_URL, url, 'enforce', 'script'); +}, 'destination: script'); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-document-reporting-endpoint.https.window.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-document-reporting-endpoint.https.window.js new file mode 100644 index 0000000000..09800db2b8 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-document-reporting-endpoint.https.window.js @@ -0,0 +1,140 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js + +// This file consists of tests for COEP reporting using Reporting-Endpoints +// header. It exclusively tests that reports can be sent to Reporting-Endpoint +// configured endpoint. +const { REMOTE_ORIGIN } = get_host_info(); + +const REPORT_ENDPOINT = token(); +const REPORT_ONLY_ENDPOINT = token(); +const FRAME_URL = `resources/reporting-empty-frame.html` + + `?pipe=header(cross-origin-embedder-policy,require-corp;report-to="endpoint")` + + `|header(cross-origin-embedder-policy-report-only,require-corp;report-to="report-only-endpoint")` + + `|header(reporting-endpoints, endpoint="/html/cross-origin-embedder-policy/resources/report.py?key=${REPORT_ENDPOINT}"\\, report-only-endpoint="/html/cross-origin-embedder-policy/resources/report.py?key=${REPORT_ONLY_ENDPOINT}")`; + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +async function fetchReports(endpoint) { + const res = await fetch(`resources/report.py?key=${endpoint}`, { + cache: 'no-store' + }); + if (res.status == 200) { + return await res.json(); + } + return []; +} + +async function fetchCoepReport( + endpoint, type, blockedUrl, contextUrl, disposition) { + blockedUrl = new URL(blockedUrl, location).href; + contextUrl = new URL(contextUrl, location).href; + const reports = await fetchReports(endpoint); + return reports.find(r => ( + r.type == 'coep' && + r.url == contextUrl && + r.body.type == type && + r.body.blockedURL === blockedUrl && + r.body.disposition === disposition)); +} + +async function checkCorpReportExists( + endpoint, blockedUrl, contextUrl, destination, disposition) { + blockedUrl = new URL(blockedUrl, location).href; + contextUrl = new URL(contextUrl, location).href; + contextUrl.replace(REPORT_ENDPOINT, "REPORT_ENDPOINT_UUID"); + contextUrl.replace(REPORT_ONLY_ENDPOINT, "REPORT_ONLY_ENDPOINT_UUID"); + const report = await fetchCoepReport( + endpoint, 'corp', blockedUrl, contextUrl, disposition); + assert_true(!!report, + `A corp report with blockedURL ${blockedUrl.split("?")[0]} ` + + `and url ${contextUrl} is not found.`); + assert_equals(report.body.destination, destination); +} + +async function checkNavigationReportExists( + endpoint, blockedUrl, contextUrl, disposition) { + blockedUrl = new URL(blockedUrl, location).href; + contextUrl = new URL(contextUrl, location).href; + contextUrl.replace(REPORT_ENDPOINT, "REPORT_ENDPOINT_UUID"); + contextUrl.replace(REPORT_ONLY_ENDPOINT, "REPORT_ONLY_ENDPOINT_UUID"); + const report = await fetchCoepReport( + endpoint, 'navigation', blockedUrl, contextUrl, disposition); + assert_true(!!report, + `A navigation report with blockedURL ${blockedUrl.split("?")[0]} ` + + `and url ${contextUrl} is not found.`); +} + +promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL; + await new Promise(resolve => { + iframe.addEventListener('load', resolve, { once: true }); + document.body.appendChild(iframe); + }); + + const url = `${REMOTE_ORIGIN}/common/text-plain.txt?${token()}`; + const init = { mode: 'no-cors', cache: 'no-store' }; + // The response comes from cross-origin, and doesn't have a CORP + // header, so it is blocked. + iframe.contentWindow.fetch(url, init).catch(() => { }); + + // Wait for reports to be uploaded. + await wait(1000); + await checkCorpReportExists( + REPORT_ENDPOINT, url, iframe.src, '', 'enforce'); + await checkCorpReportExists( + REPORT_ONLY_ENDPOINT, url, iframe.src, '', 'reporting'); +}, 'subresource CORP'); + +promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL; + await new Promise(resolve => { + iframe.addEventListener('load', resolve, { once: true }); + document.body.appendChild(iframe); + }); + + const url = `${REMOTE_ORIGIN}/common/blank.html?${token()}`; + // The nested frame comes from cross-origin and doesn't have a CORP + // header, so it is blocked. + const nested = iframe.contentWindow.document.createElement('iframe'); + nested.src = url; + iframe.contentWindow.document.body.appendChild(nested); + + // Wait for reports to be uploaded. + await wait(1000); + await checkCorpReportExists( + REPORT_ENDPOINT, url, iframe.src, 'iframe', 'enforce'); + await checkCorpReportExists( + REPORT_ONLY_ENDPOINT, url, iframe.src, 'iframe', 'reporting'); +}, 'navigation CORP on cross origin'); + +promise_test(async (t) => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL; + const targetUrl = `/common/blank.html?${token()}`; + iframe.addEventListener('load', t.step_func(() => { + const nested = iframe.contentDocument.createElement('iframe'); + nested.src = targetUrl; + // |nested| doesn't have COEP whereas |iframe| has, so it is blocked. + iframe.contentDocument.body.appendChild(nested); + }), { once: true }); + + document.body.appendChild(iframe); + + // Wait for reports to be uploaded. + await wait(1000); + await checkNavigationReportExists( + REPORT_ENDPOINT, targetUrl, iframe.src, 'enforce'); + await checkNavigationReportExists( + REPORT_ONLY_ENDPOINT, targetUrl, iframe.src, 'reporting'); +}, 'navigation CORP on same origin'); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html new file mode 100644 index 0000000000..39c3de7076 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html @@ -0,0 +1,209 @@ +<!doctype html> +<html> +<meta name="timeout" content="long"> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +// This file consists of tests for COEP reporting. The tests make COEP +// violations and see whether reports are sent to the network as specified. +// We only have basic tests in this file - one for each kind of reports, +// because we can also test the reporting functionality with ReportingObserver, +// and that way is faster, easier to debug, and less flaky. +// +// For more detailed tests and tests with workers, see tests in other files +// such as +// - reporting-navigation.https.html +// - reporting-subresource-corp.https.html +// - cache-storage-reporting*.https.html +// . + +const { REMOTE_ORIGIN } = get_host_info(); +const BASE = new URL("resources", location).pathname +const FRAME_URL = `resources/reporting-empty-frame.html` + + `?pipe=header(cross-origin-embedder-policy,require-corp;report-to="endpoint")` + + `|header(cross-origin-embedder-policy-report-only,require-corp;report-to="report-only-endpoint")`; +const WORKER_URL = `resources/shared-worker.js` + + '?pipe=header(cross-origin-embedder-policy,require-corp;report-to="endpoint")' + + `|header(cross-origin-embedder-policy-report-only,require-corp;report-to="report-only-endpoint")`; +const REPORT_UUID = "4d8b6d86-c9a8-47c1-871b-111169a8f79c"; +const REPORT_ONLY_UUID = "5d7c1e33-ef88-43c2-9ca3-c67ff300b8c2"; + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +async function fetchReports(endpoint) { + const res = await fetch(`resources/report.py?key=${endpoint}`, {cache: 'no-store'}); + if (res.status == 200) { + return await res.json(); + } + return []; +} + +async function checkCorpReportExistence(endpoint, blockedUrl, contextUrl, destination, disposition) { + blockedUrl = new URL(blockedUrl, location).href; + contextUrl = new URL(contextUrl, location).href; + + const timeout = 3000; + const retryDelay = 200; + for (let i = 0; i * retryDelay < timeout; i++) { + const reports = await fetchReports(endpoint); + for (const report of reports) { + if (report.type !== 'coep' || report.url !== contextUrl || + report.body.type !== 'corp') { + continue; + } + if (report.body.blockedURL === blockedUrl && + report.body.disposition === disposition) { + assert_equals(report.body.destination, destination); + return; + } + } + await wait(retryDelay); + } + assert_unreached(`A report whose blockedURL is ${blockedUrl.split("?")[0]} and url is ${contextUrl} is not found.`); +} + +async function checkNavigationReportExistence(endpoint, blockedUrl, contextUrl, disposition) { + blockedUrl = new URL(blockedUrl, location).href; + contextUrl = new URL(contextUrl, location).href; + const timeout = 3000; + const retryDelay = 200; + for (let i = 0; i * retryDelay < timeout; i++) { + const reports = await fetchReports(endpoint); + for (const report of reports) { + if (report.type !== 'coep' || report.url !== contextUrl || + report.body.type !== 'navigation') { + continue; + } + if (report.body.blockedURL === blockedUrl && + report.body.disposition === disposition) { + return; + } + } + await wait(retryDelay); + } + assert_unreached(`A report whose blockedURL is ${blockedUrl.split("?")[0]} and url is ${contextUrl} is not found.`); +} + +promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener('load', resolve, {once: true}); + }); + + const url = `${REMOTE_ORIGIN}/common/text-plain.txt?${token()}`; + const init = { mode: 'no-cors', cache: 'no-store' }; + // The response comes from cross-origin, and doesn't have a CORP + // header, so it is blocked. + iframe.contentWindow.fetch(url, init).catch(() => {}); + + await checkCorpReportExistence(REPORT_UUID, url, iframe.src, '', 'enforce'); + await checkCorpReportExistence( + REPORT_ONLY_UUID, url, iframe.src, '', 'reporting'); +}, 'subresource CORP'); + +promise_test(async t => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener('load', resolve, {once: true}); + }); + + const w = iframe.contentWindow; + + function attachFrame(url) { + const frame = w.document.createElement('iframe'); + frame.src = url; + w.document.body.appendChild(frame); + } + + const url = `${REMOTE_ORIGIN}/common/blank.html?${token()}`; + // The nested frame comes from cross-origin and doesn't have a CORP + // header, so it is blocked. + attachFrame(url); + + await checkCorpReportExistence( + REPORT_UUID, url, iframe.src, 'iframe', 'enforce'); + await checkCorpReportExistence( + REPORT_ONLY_UUID, url, iframe.src, 'iframe', 'reporting'); +}, 'navigation CORP'); + +promise_test(async (t) => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL; + const targetUrl = `/common/blank.html?${token()}`; + iframe.addEventListener('load', t.step_func(() => { + const nested = iframe.contentDocument.createElement('iframe'); + nested.src = targetUrl; + // |nested| doesn't have COEP whereas |iframe| has, so it is blocked. + iframe.contentDocument.body.appendChild(nested); + }), {once: true}); + + document.body.appendChild(iframe); + + await checkNavigationReportExistence( + REPORT_UUID, targetUrl, iframe.src, 'enforce'); + await checkNavigationReportExistence( + REPORT_ONLY_UUID, targetUrl, iframe.src, 'reporting'); +}, 'COEP violation on nested frame navigation'); + +promise_test(async (t) => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = 'resources/reporting-empty-frame-multiple-headers.html.asis'; + const targetUrl = `/common/blank.html?${token()}`; + + iframe.addEventListener('load', t.step_func(() => { + const nested = iframe.contentDocument.createElement('iframe'); + nested.src = targetUrl; + // |nested| doesn't have COEP whereas |iframe| has, so it is blocked. + iframe.contentDocument.body.appendChild(nested); + }), {once: true}); + + document.body.appendChild(iframe); + + await checkNavigationReportExistence( + REPORT_UUID, targetUrl, iframe.src, 'enforce'); + await checkNavigationReportExistence( + REPORT_ONLY_UUID, targetUrl, iframe.src, 'reporting'); + +}, 'Two COEP headers, split inside report-to value'); + +// Shared worker do not support observer currently, so add test for endpoint +// here. +promise_test(async (t) => { + const iframe = document.createElement('iframe'); + t.add_cleanup(() => iframe.remove()); + + iframe.src = FRAME_URL; + const targetUrl = `${REMOTE_ORIGIN}/common/blank.html?${token()}`; + document.body.appendChild(iframe); + + const worker = new iframe.contentWindow.SharedWorker(WORKER_URL); + worker.port.start(); + const script = + `fetch('${targetUrl}', {mode: 'no-cors', cache: 'no-store'}).catch(e => {});`; + worker.addEventListener('error', t.unreached_func('Worker.onerror')); + worker.port.postMessage(script); + + await checkCorpReportExistence( + REPORT_UUID, targetUrl, WORKER_URL, 'iframe', 'enforce'); + await checkCorpReportExistence( + REPORT_ONLY_UUID, targetUrl, WORKER_URL, 'iframe', 'reporting'); +}, 'Shared worker fetch'); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html.sub.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html.sub.headers new file mode 100644 index 0000000000..fe2f651dae --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-endpoint.https.html.sub.headers @@ -0,0 +1 @@ +Reporting-Endpoints: endpoint="https://{{host}}:{{ports[https][0]}}//html/cross-origin-embedder-policy/resources/report.py?key=4d8b6d86-c9a8-47c1-871b-111169a8f79c", report-only-endpoint="/html/cross-origin-embedder-policy/resources/report.py?key=5d7c1e33-ef88-43c2-9ca3-c67ff300b8c2" diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html new file mode 100644 index 0000000000..331ad898eb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-frame-owner.https.html @@ -0,0 +1,87 @@ +<!doctype html> +<html> +<head> +<title>Check COEP reports are sent to iframe for 'new Worker()' failure</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const {ORIGIN} = get_host_info(); +const RESOURCES_PATH= new URL("resources", location).pathname; +const iframe_path = "worker-owner-frame.html?pipe="; +const worker_path = "universal-worker.js?pipe="; + +const coep_header= { + "coep-none" : "", + "coep-report-only" : + "header(Cross-Origin-Embedder-Policy-Report-Only,require-corp)", + "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)", +}; + +function checkReport(report, url, blocked_url, disposition) { + assert_equals(report.type, "coep"); + assert_equals(report.url, url); + assert_equals(report.body.type, "worker initialization"); + assert_equals(report.body.blockedURL, blocked_url); + assert_equals(report.body.disposition, disposition); +} + +// Test parameters: +// - `owner_coep` the COEP header of the iframe document's response. +// - `worker_coep` the COEP header of the DedicatedWorker's script response. +// +// Test expectations: +// - `length` the length of reports. +// - `disposition` the disposition in a report's body. Empty string if the +// length of reports is expected to be 0. +function check( + // Test parameters: + owner_coep, + worker_coep, + // Test expectations: + length, + disposition) { + promise_test(async (t) => { + const worker_url = worker_path + coep_header[worker_coep]; + const iframe_url = iframe_path + coep_header[owner_coep]; + const iframe = await with_iframe("./resources/" + iframe_url); + t.add_cleanup(() => iframe.remove()); + + const iframe_response = new Promise(resolve => window.onmessage = resolve); + iframe.contentWindow.startWorkerAndObserveReports(worker_url, length > 0); + + const {data} = await iframe_response; + assert_equals(data.length, length); + if (data.length > 0) { + const blocked_url = `${ORIGIN}${RESOURCES_PATH}/${worker_url}`; + const url = `${ORIGIN}${RESOURCES_PATH}/${iframe_url}`; + checkReport( + data[0], + url, + blocked_url, + disposition + ); + } + }, `Reporting to ${owner_coep} frame with ${worker_coep} worker`); +} + +// ----------------------------------------------------------------------------- +// owner_coep , worker_coep , length , disposition +// ----------------------------------------------------------------------------- +check("coep-none" , "coep-none" , 0 , ""); +check("coep-none" , "coep-report-only" , 0 , ""); +check("coep-none" , "coep-require-corp" , 0 , ""); +check("coep-report-only" , "coep-none" , 1 , "reporting"); +check("coep-report-only" , "coep-report-only" , 1 , "reporting"); +check("coep-report-only" , "coep-require-corp" , 0 , ""); +check("coep-require-corp" , "coep-none" , 1 , "enforce"); +check("coep-require-corp" , "coep-report-only" , 1 , "enforce"); +check("coep-require-corp" , "coep-require-corp" , 0 , ""); + +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html new file mode 100644 index 0000000000..c0010876fa --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/reporting-to-worker-owner.https.html @@ -0,0 +1,89 @@ +<!doctype html> +<html> +<head> +<title>Check COEP reports are sent to parent worker for 'new Worker()' failure</title> +</head> +<body> +<script src="/common/get-host-info.sub.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const {ORIGIN} = get_host_info(); +const RESOURCES_PATH= new URL("resources", location).pathname; +const parent_worker_path = "worker-owner.js?pipe="; +const worker_path = "universal-worker.js?pipe="; + +const coep_header= { + "coep-none" : "", + "coep-report-only" : + "header(Cross-Origin-Embedder-Policy-Report-Only,require-corp)", + "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)", +}; + +function checkReport(report, url, blocked_url, disposition) { + assert_equals(report.type, "coep"); + assert_equals(report.url, url); + assert_equals(report.body.type, "worker initialization"); + assert_equals(report.body.blockedURL, blocked_url); + assert_equals(report.body.disposition, disposition); +} + +// Test parameters: +// - `owner_coep` the COEP header of the parent DedicatedWorker's script +// response. +// - `worker_coep` the COEP header of the DedicatedWorker's script response. +// +// Test expectations: +// - `length` the length of reports. +// - `disposition` the disposition in a report's body. Empty string if the +// length of reports is expected to be 0. +function check( + // Test parameters: + owner_coep, + worker_coep, + // Test expectations: + length, + disposition) { + promise_test(async (t) => { + const worker_url = worker_path + coep_header[worker_coep]; + const parent_worker_url = parent_worker_path + coep_header[owner_coep]; + const parent_worker = new Worker('./resources/' + parent_worker_url); + + const worker_response = + new Promise(resolve => parent_worker.onmessage = resolve); + parent_worker.postMessage( + {worker_url: worker_url, wait_for_report: length > 0}); + + const {data} = await worker_response; + assert_equals(data.length, length); + if (data.length > 0) { + const blocked_url = `${ORIGIN}${RESOURCES_PATH}/${worker_url}`; + const url = `${ORIGIN}${RESOURCES_PATH}/${parent_worker_url}`; + checkReport( + data[0], + url, + blocked_url, + disposition + ); + } + }, `Reporting to ${owner_coep} worker with ${worker_coep} worker`); +} + +// ----------------------------------------------------------------------------- +// owner_coep , worker_coep , length , disposition +// ----------------------------------------------------------------------------- +check("coep-none" , "coep-none" , 0 , ""); +check("coep-none" , "coep-report-only" , 0 , ""); +check("coep-none" , "coep-require-corp" , 0 , ""); +check("coep-report-only" , "coep-none" , 1 , "reporting"); +check("coep-report-only" , "coep-report-only" , 1 , "reporting"); +check("coep-report-only" , "coep-require-corp" , 0 , ""); +check("coep-require-corp" , "coep-none" , 1 , "enforce"); +check("coep-require-corp" , "coep-report-only" , 1 , "enforce"); +check("coep-require-corp" , "coep-require-corp" , 0 , ""); + +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html new file mode 100644 index 0000000000..945333b83d --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html @@ -0,0 +1,48 @@ +<!doctype html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> + +promise_test(t => { + return new Promise(resolve => { + window.addEventListener("DOMContentLoaded", resolve); + }); +}, "Wait for the DOM to be built."); + +promise_test(async t => { + let iframe = document.createElement("iframe"); + let iframe_loaded = new Promise(resolve => iframe.onload = resolve); + iframe.src = "about:blank"; + document.body.appendChild(iframe); + + // The about:blank document can load. + await iframe_loaded; + assert_not_equals(iframe.contentDocument, null); +}, "about:blank can always be embedded by a 'require-corp' document"); + +promise_test(async t => { + let iframe_C = document.createElement("iframe"); + let iframe_B = document.createElement("iframe"); + iframe_B.src = "about:blank"; + iframe_C.src = "/common/blank.html"; + let iframe_B_loaded = new Promise(resolve => iframe_B.onload = resolve); + document.body.appendChild(iframe_B); + + // The about:blank frame must be able to load. + await iframe_B_loaded; + assert_not_equals(iframe_B.contentDocument, null); + iframe_B.contentDocument.body.appendChild(iframe_C); + + + // The document nested under about:blank must not load because it does not + // specify the Cross-Origin-Embedder-Policy: require-corp header. + // An error page must be displayed instead. + // See https://github.com/whatwg/html/issues/125 for why a timeout is used + // here. Long term all network error handling should be similar and have a + // reliable event. + assert_equals(iframe_C.contentWindow.location.href, "about:blank"); + assert_not_equals(iframe_C.contentDocument, null); + await t.step_wait(() => iframe_C.contentDocument === null); +}, "A(B(C)) A=require-corp, B=about:blank, C=no-require-corp => C can't load"); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-blank.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html new file mode 100644 index 0000000000..5d06286d91 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html @@ -0,0 +1,48 @@ +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> + +promise_test(t => { + return new Promise(resolve => { + window.addEventListener("DOMContentLoaded", resolve); + }); +}, "Wait for the DOM to be built."); + +promise_test(async t => { + let iframe = document.createElement("iframe"); + let iframe_loaded = new Promise(resolve => iframe.onload = resolve); + iframe.srcdoc = "loaded document"; + document.body.appendChild(iframe); + + // The about:srcdoc document can load. + await iframe_loaded; + assert_not_equals(iframe.contentDocument, null); + assert_equals(iframe.contentDocument.body.innerText, "loaded document"); +}, "about:srcdoc can always be embedded by a 'require-corp' document"); + +promise_test(async t => { + let iframe_C = document.createElement("iframe"); + let iframe_B = document.createElement("iframe"); + iframe_B.srcdoc = "dummy content"; + iframe_C.src = "/common/blank.html"; + let iframe_B_loaded = new Promise(resolve => iframe_B.onload = resolve); + document.body.appendChild(iframe_B); + + // The about:srcdoc frame must be able to load. + await iframe_B_loaded; + assert_not_equals(iframe_B.contentDocument, null); + assert_equals(iframe_B.contentDocument.body.innerText, "dummy content"); + iframe_B.contentDocument.body.appendChild(iframe_C); + + // The document nested under about:srcdoc must not load because it does not + // specify the Cross-Origin-Embedder-Policy: require-corp header. + // An error page must be displayed instead. + // See https://github.com/whatwg/html/issues/125 for why a timeout is used + // here. Long term all network error handling should be similar and have a + // reliable event. + assert_equals(iframe_C.contentWindow.location.href, "about:blank"); + assert_not_equals(iframe_C.contentDocument, null); + await t.step_wait(() => iframe_C.contentDocument === null); +}, "A(B(C)) A=require-corp, B=about:srcdoc, C=no-require-corp => C can't load"); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-about-srcdoc.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html new file mode 100644 index 0000000000..269698bc1a --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html @@ -0,0 +1,74 @@ +<!doctype html> +<html> +<title> Images on a page Cross-Origin-Embedder-Policy: require-corp should load the same from the cache or network</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script> + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +// +// This test loads a same-origin iframe resources/load-corp-images.html with +// Cross-Origin-Embedder-Policy: require-corp +// The iframe loads two cross origin images, one with a +// Cross-Origin-Resource-Policy: cross-origin header, and one without. +// We expect the image with the header to load successfully and the one without +// to fail to load. +// After the first load we then reload the iframe, with the same expectations +// for the image loads when they are loaded from the cache. +// + +const RUNS = ["NETWORK", "CACHED"]; +const RESOURCE_DESC = ["No CORP image", "CORP image"]; + +let EXPECTED_LOADS = { + [`${RUNS[0]} - ${RESOURCE_DESC[0]}`]: false, + [`${RUNS[0]} - ${RESOURCE_DESC[1]}`]: true, + [`${RUNS[1]} - ${RESOURCE_DESC[0]}`]: false, + [`${RUNS[1]} - ${RESOURCE_DESC[1]}`]: true, +} + +let TESTS = {}; +for (let t in EXPECTED_LOADS) { + TESTS[t] = async_test(t); +} + +window.addEventListener("load", async () => { + const t = async_test("main_test"); + const iframe = document.createElement("iframe"); + // The token attribute is used to ensure the resource has never been seen by + // the HTTP cache. This can be useful if the cache isn't properly flushed in + // between two tests. + iframe.src = `resources/load-corp-images.html?revalidate=false&token=${token()}`; + let runCount = 0; + window.addEventListener("message", (event) => { + // After the first done event we reload the iframe. + if (event.data.done) { + ++runCount; + if (runCount < RUNS.length) { + iframe.contentWindow.location.reload(); + } else { + // After the second done event the test is finished. + t.done(); + } + return; + } + + // Check that each image either loads or doesn't based on the expectations + let testName = `${RUNS[runCount]} - ${event.data.corp ? RESOURCE_DESC[1] : RESOURCE_DESC[0]}`; + let test = TESTS[testName]; + test.step(() => { + assert_equals(event.data.loaded, EXPECTED_LOADS[testName], `${testName} should ${EXPECTED_LOADS[testName] ? "" : "not"} succeed`); + }); + test.done(); + }, false); + document.body.appendChild(iframe); +}); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-cached-images.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html new file mode 100644 index 0000000000..489230a776 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html @@ -0,0 +1,179 @@ +<!doctype html> +<html> +<title> Retrieve resources from CacheStorage with Cross-Origin-Embedder-Policy: require-corp</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> + +/* + This document has the header Cross-Origin-Embedder-Policy: require-corp. + Cross-Origin Embedder Policy Editor's draft: https://mikewest.github.io/corpp/ + + This test is retrieving same-origin and cross-origin resources from the + CacheStorage. The resources are generated from the ServiceWorker or from the + network with the header Cross-Origin-Resource-Policy being one of: + - 'same-origin' + - 'cross-origin' + - <undefined> +*/ + +promise_test(async (t) => { + const SCOPE = new URL(location.href).pathname; + const SCRIPT = + 'resources/sw-store-to-cache-storage.js?' + + `pipe=header(service-worker-allowed,${SCOPE})`; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => reg.unregister()); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +function local(path) { + return new URL(path, location.origin); +} + +// Send a message to the currently active ServiceWorker and wait for its +// response. +function executeCommandInServiceWorker(command) { + return new Promise(resolve => { + navigator.serviceWorker.addEventListener('message', e => resolve(e.data)); + navigator.serviceWorker.controller.postMessage(command); + }); +} + +// Try loading an image from a |response|. Return a Promise resolving or +// rejecting depending on the image loading result. +const loadFailure = {name: "Image.onerror"}; +function readImageFromResponse(response) { + return new Promise((resolve, reject) => { + const img = document.createElement("img"); + img.onload = resolve.bind(this, ""); + img.onerror = reject.bind(this, loadFailure); + response.blob().then(blob => { + img.src = URL.createObjectURL(blob); + document.body.appendChild(img); + }) + }) +} + +const image_path = "/images/blue.png?pipe="; + +const corp_header = { + "":"", + "corp-undefined": "", + "corp-same-origin": "|header(Cross-Origin-Resource-Policy,same-origin)", + "corp-cross-origin": "|header(Cross-Origin-Resource-Policy,cross-origin)", +} + +const cors_header = { + "":"", + "cors-disabled": "", + "cors-enabled": "|header(Access-Control-Allow-Origin,*)", +} + +function test( + // Test parameters: + request_source, request_origin, request_mode, response_cors, response_corp, + // Test expectations: + response_stored, response_type) { + promise_test(async (t) => { + // 0. Start from an empty CacheStorage. + await caches.delete("v1"); + + // 1. Store a cross-origin no-cors response generated from the SW into the + // CacheStorage. + const path = image_path + + corp_header[response_corp] + + cors_header[response_cors]; + const url = (request_origin === "same-origin" ? local : remote)(path); + const command = { + url: url.href, + mode: request_mode, + source: request_source, + }; + + assert_equals(await executeCommandInServiceWorker(command), response_stored); + if (response_stored === "not-stored") { + return; + } + + // 2. Retrieve it from the CacheStorage. + const cache = await caches.open('v1'); + + if (response_type === 'error') { + await promise_rejects_js(t, TypeError, cache.match(url)); + return; + } + + const response = await cache.match(url); + + assert_equals(response.type, response_type); + + if (request_source === "service-worker") { + assert_equals("foo", await response.text()); + return; + } + + // Opaque response can't be read from the document. + if (response_type === "opaque") { + await promise_rejects_exactly(t, loadFailure, readImageFromResponse(response)); + return; + } + + await readImageFromResponse(response); + }, `Fetch ${request_origin} ${request_mode} ${response_cors} ${response_corp} from ${request_source} and CacheStorage.`) +} + +// Responses generated from the ServiceWorker. +{ + test("service-worker", "cross-origin", "cors", "", "", "stored", "default"); + test("service-worker", "cross-origin", "no-cors", "", "", "stored", "default"); + test("service-worker", "same-origin", "cors", "", "", "stored", "default"); + test("service-worker", "same-origin", "no-cors", "", "", "stored", "default"); +} + +// Responses generated from a same-origin server. +{ + const t = test.bind(this, "network", "same-origin"); + t("cors", "cors-disabled", "corp-cross-origin", "stored", "basic"); + t("cors", "cors-disabled", "corp-same-origin", "stored", "basic"); + t("cors", "cors-disabled", "corp-undefined", "stored", "basic"); + t("cors", "cors-enabled", "corp-cross-origin", "stored", "basic"); + t("cors", "cors-enabled", "corp-same-origin", "stored", "basic"); + t("cors", "cors-enabled", "corp-undefined", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-cross-origin", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-same-origin", "stored", "basic"); + t("no-cors", "cors-disabled", "corp-undefined", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-cross-origin", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-same-origin", "stored", "basic"); + t("no-cors", "cors-enabled", "corp-undefined", "stored", "basic"); +} + +// Responses generated from a cross-origin server. +{ + const t = test.bind(this, "network", "cross-origin"); + t("cors", "cors-disabled", "corp-cross-origin", "not-stored"); + t("cors", "cors-disabled", "corp-same-origin", "not-stored"); + t("cors", "cors-disabled", "corp-undefined", "not-stored"); + t("cors", "cors-enabled", "corp-cross-origin", "stored", "cors"); + t("cors", "cors-enabled", "corp-same-origin", "stored", "cors"); + t("cors", "cors-enabled", "corp-undefined", "stored", "cors"); + t("no-cors", "cors-disabled", "corp-cross-origin", "stored", "opaque"); + t("no-cors", "cors-disabled", "corp-same-origin", "not-stored"); + t("no-cors", "cors-disabled", "corp-undefined", "stored", "error"); + t("no-cors", "cors-enabled", "corp-cross-origin", "stored", "opaque"); + t("no-cors", "cors-enabled", "corp-same-origin", "not-stored"); + t("no-cors", "cors-enabled", "corp-undefined", "stored", "error"); +} + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-load-from-cache-storage.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-revalidated-images.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-revalidated-images.https.html new file mode 100644 index 0000000000..420190aad3 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-revalidated-images.https.html @@ -0,0 +1,76 @@ +<!doctype html> +<html> +<title> Images on a page Cross-Origin-Embedder-Policy: require-corp should load the same from the cache or network, even with revalidation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script> + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +// +// This test loads a same-origin iframe resources/load-corp-images.html with +// Cross-Origin-Embedder-Policy: require-corp +// The iframe loads two cross origin images, one with a +// Cross-Origin-Resource-Policy: cross-origin header, and one without. +// We expect the image with the header to load successfully and the one without +// to fail to load. +// After the first load we then reload the iframe, with the same expectations +// for the image loads when they are loaded from the cache. Because of the +// revalidate directive, we will receive a 304 response instead of directly +// using the cache response. +// + +const RUNS = ["NETWORK", "CACHED"]; +const RESOURCE_DESC = ["No CORP image", "CORP image"]; + +let EXPECTED_LOADS = { + [`${RUNS[0]} - ${RESOURCE_DESC[0]}`]: false, + [`${RUNS[0]} - ${RESOURCE_DESC[1]}`]: true, + [`${RUNS[1]} - ${RESOURCE_DESC[0]}`]: false, + [`${RUNS[1]} - ${RESOURCE_DESC[1]}`]: true, +} + +let TESTS = {}; +for (let t in EXPECTED_LOADS) { + TESTS[t] = async_test(t); +} + +window.addEventListener("load", async () => { + const t = async_test("main_test"); + const iframe = document.createElement("iframe"); + // The token attribute is used to ensure the resource has never been seen by + // the HTTP cache. This can be useful if the cache isn't properly flushed in + // between two tests. + iframe.src = `resources/load-corp-images.html?revalidate=true&token=${token()}`; + let runCount = 0; + window.addEventListener("message", (event) => { + // After the first done event we reload the iframe. + if (event.data.done) { + ++runCount; + if (runCount < RUNS.length) { + iframe.contentWindow.location.reload(); + } else { + // After the second done event the test is finished. + t.done(); + } + return; + } + + // Check that each image either loads or doesn't based on the expectations + let testName = `${RUNS[runCount]} - ${event.data.corp ? RESOURCE_DESC[1] : RESOURCE_DESC[0]}`; + let test = TESTS[testName]; + test.step(() => { + assert_equals(event.data.loaded, EXPECTED_LOADS[testName], `${testName} should ${EXPECTED_LOADS[testName] ? "" : "not"} succeed`); + }); + test.done(); + }, false); + document.body.appendChild(iframe); +}); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-none.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-none.https.html new file mode 100644 index 0000000000..451761e818 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-none.https.html @@ -0,0 +1,92 @@ +<!doctype html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const SCOPE = new URL(location.href).pathname; +const SCRIPT = + 'resources/sw.js?' + + `pipe=header(service-worker-allowed,${SCOPE})|` + + 'header(cross-origin-embedder-policy,require-corp)'; + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN + '/html/cross-origin-embedder-policy/'); +} + +promise_test(async (t) => { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => { + reg.unregister(); + }); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +promise_test(async (t) => { + await fetch('resources/nothing-same-origin-corp.txt', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await fetch('/common/blank.html', {mode: 'no-cors'}); +}, 'making a same-origin request for no CORP'); + +promise_test(async (t) => { + await fetch('resources/nothing-cross-origin-corp.js', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt'), {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt?passthrough'), + {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch(remote('/common/blank.html?passthrough'), {mode: 'no-cors'}); +}, 'making a cross-origin request for no CORP [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js?passthrough'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'cors'})); +}, 'making a cross-origin request with CORS without ACAO'); + +promise_test(async (t) => { + const URL = remote( + '/common/blank.html?pipe=header(access-control-allow-origin,*)'); + await fetch(URL, {mode: 'cors'}); +}, 'making a cross-origin request with CORS'); + +promise_test(async (t) => { + const URL = remote('/fetch/api/resources/preflight.py?allow_headers=hoge'); + await fetch(URL, {mode: 'cors', headers: {'hoge': 'fuga'}}); +}, 'making a cross-origin request with CORS-preflight'); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html new file mode 100644 index 0000000000..c270e3a0d6 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +const SCOPE = new URL(location.href).pathname; +const SCRIPT = + 'resources/sw.js?' + + `pipe=header(service-worker-allowed,${SCOPE})|` + + 'header(cross-origin-embedder-policy,require-corp)'; + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN + '/html/cross-origin-embedder-policy/'); +} + +promise_test(async (t) => { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => { + reg.unregister(); + }); + await new Promise(resolve => { + navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}, 'setting up'); + +promise_test(async (t) => { + await fetch('resources/nothing-same-origin-corp.txt', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await fetch('/common/blank.html', {mode: 'no-cors'}); +}, 'making a same-origin request for no CORP'); + +promise_test(async (t) => { + await fetch('resources/nothing-cross-origin-corp.js', {mode: 'no-cors'}); +}, 'making a same-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt'), {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('resources/nothing-same-origin-corp.txt?passthrough'), + {mode: 'no-cors'})); +}, 'making a cross-origin request for CORP: same-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, + fetch(remote('/common/blank.html?passthrough'), {mode: 'no-cors'})); +}, 'making a cross-origin request for no CORP [PASS THROUGH]'); + +promise_test(async (t) => { + await fetch( + remote('resources/nothing-cross-origin-corp.js?passthrough'), + {mode: 'no-cors'}); +}, 'making a cross-origin request for CORP: cross-origin [PASS THROUGH]'); + +promise_test(async (t) => { + await promise_rejects_js( + t, TypeError, fetch(remote('/common/blank.html'), {mode: 'cors'})); +}, 'making a cross-origin request with CORS without ACAO'); + +promise_test(async (t) => { + const URL = remote( + '/common/blank.html?pipe=header(access-control-allow-origin,*)'); + await fetch(URL, {mode: 'cors'}); +}, 'making a cross-origin request with CORS'); + +promise_test(async (t) => { + const URL = remote('/fetch/api/resources/preflight.py?allow_headers=hoge'); + await fetch(URL, {mode: 'cors', headers: {'hoge': 'fuga'}}); +}, 'making a cross-origin request with CORS-preflight'); +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw-from-require-corp.https.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw.https.html new file mode 100644 index 0000000000..05272d41a4 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-sw.https.html @@ -0,0 +1,53 @@ +<!doctype html> +<title>Cross Origin Embedder Policy: requests initiated from a service worker with 'require-corp'</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const SCRIPT = 'resources/require-corp-sw.js'; +const SCOPE = 'resources/in-scope'; +let worker = null; + +promise_test(async t => { + const registration = + await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + promise_test(async t => registration.unregister(), 'Clean up global state'); + worker = registration.installing; + await wait_for_state(t, worker, 'activated'); +}, 'Set up global state'); + +promise_test(async t => { + const p = new Promise(resolve => + navigator.serviceWorker.addEventListener('message', resolve, + {once: true})); + worker.postMessage('WithCorp'); + assert_equals((await p).data, 'opaque'); +}, "fetch() to 'CORP: cross-origin' response should succeed."); + +promise_test(async t => { + const p = new Promise(resolve => + navigator.serviceWorker.addEventListener('message', resolve, + {once: true})); + worker.postMessage('WithoutCorp'); + assert_equals((await p).data, 'Exception: TypeError'); +}, "fetch() to no CORP response should not succeed."); + +promise_test(async t => { + const scope = `${SCOPE}-2`; + await service_worker_unregister(t, scope); + const promise = navigator.serviceWorker.register( + 'resources/require-corp-sw-import-scripts.js', {scope}); + await promise_rejects_js(t, TypeError, promise, 'register() should fail.'); +}, 'importScripts() fails for a script with no corp.'); + +promise_test(async t => { + const scope = `${SCOPE}-3`; + await service_worker_unregister(t, scope); + const registration = await navigator.serviceWorker.register( + 'resources/require-corp-sw-import-scripts.js?corp=cross-origin', {scope}); + t.add_cleanup(() => registration.unregister()); +}, 'importScripts() succeeds for a script with corp: cross-origin.'); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-worker-script-revalidation.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-worker-script-revalidation.html new file mode 100644 index 0000000000..74794967fb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp-worker-script-revalidation.html @@ -0,0 +1,25 @@ +<!doctype html> +<title>COEP and dedicated worker</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/worker-support.js"></script> +<body> +<script> + +promise_test(async (t) => { + const worker1 = new Worker("/html/cross-origin-embedder-policy/resources/dedicated-worker-supporting-revalidation.py"); + worker1.onerror = t.unreached_func('Worker.onerror should not be called for first worker'); + worker1.postMessage("foo"); + const result1 = await waitForMessage(worker1); + assert_equals(result1.data, 'LOADED'); + + // Load the worker a second time, which should trigger revalidation of the cached resource. + const worker2 = new Worker("/html/cross-origin-embedder-policy/resources/dedicated-worker-supporting-revalidation.py"); + worker2.onerror = t.unreached_func('Worker.onerror should not be called worker second worker'); + worker2.postMessage("foo"); + const result2 = await waitForMessage(worker2); + assert_equals(result2.data, 'LOADED'); +}, 'COEP: require-corp with revalidated worker script'); +</script> +</body> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html new file mode 100644 index 0000000000..ad64a485c0 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html @@ -0,0 +1,251 @@ +<!doctype html> +<meta name="timeout" content="long"> +<title>Cross-Origin-Embedder-Policy header and nested navigable resource without such header</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> <!-- Use token() to allow running tests in parallel --> +<div id=log></div> +<script> +const HOST = get_host_info(); +const BASE = new URL("resources", location).pathname; + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "/common/blank.html"; + document.body.append(frame); + // Make sure the iframe didn't load. See https://github.com/whatwg/html/issues/125 for why a + // timeout is used here. Long term all network error handling should be similar and have a + // reliable event. + assert_equals(frame.contentDocument.body.localName, "body"); + t.step_wait_func_done(() => frame.contentDocument === null); +}, `"require-corp" top-level: navigating a frame to "none" should fail`); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + const bc = new BroadcastChannel(token()); + bc.onmessage = t.step_func((event) => { + let payload = event.data; + assert_equals(payload, "loaded"); + t.step_wait_func_done(() => frame.contentDocument === null); + }); + frame.src = `resources/navigate-require-corp.sub.html?channelName=${bc.name}&to=/common/blank.html`; + document.body.append(frame); + assert_equals(frame.contentDocument.body.localName, "body"); +}, `"require-corp" top-level: navigating a frame from "require-corp" to "none" should fail`); + +async_test(t => { + let pageLoaded = false; + const bc = new BroadcastChannel(token()); + let finished = false; + bc.onmessage = t.step_func((event) => { + let payload = event.data; + assert_equals(payload, "loaded"); + pageLoaded = true; + }); + + const bc2 = new BroadcastChannel(token()); + bc2.onmessage = t.step_func_done((event) => { + let payload = event.data; + assert_equals(payload, "loaded"); + assert_equals(pageLoaded, true); + }); + + const win = window.open(`resources/navigate-none.sub.html?channelName=${bc.name}&to=navigate-none.sub.html?channelName=${bc2.name}`, "_blank", "noopener"); + assert_equals(win, null); +}, `"require-corp" top-level: creating a noopener "none" popup should succeed`); + +async_test(t => { + let pageLoaded = false; + const bc = new BroadcastChannel(token()); + bc.onmessage = t.step_func_done((event) => { + pageLoaded = true; + let payload = event.data; + assert_equals(payload, "loaded"); + }); + + const win = window.open(`resources/navigate-none.sub.html?channelName=${bc.name}&to=/common/blank.html`, "_blank"); + t.add_cleanup(() => win.close()); +}, `"require-corp" top-level: creating a "none" popup should succeed.`); + +[ + { + "name": "", + "title": "as popup" + }, + { + "name": "noopener", + "title": "as noopener popup" + }, + { + "name": "clear opener", + "title": "as popup with opener set to null" + } +].forEach(({name, title}) => { + async_test(t => { + let pageLoaded = false; + const bc = new BroadcastChannel(token()); + bc.onmessage = t.step_func(event => { + pageLoaded = true; + const payload = event.data; + assert_equals(payload, "loaded"); + }); + + const bc2 = new BroadcastChannel(token()); + bc2.onmessage = t.step_func_done(event => { + const payload = event.data; + assert_equals(payload, "loaded"); + assert_equals(pageLoaded, true); + }); + + let clearOpener = ""; + if (name === "clear opener") { + clearOpener = "&clearOpener=true" + } + + let noopener = undefined; + if (name === "noopener") { + noopener = "noopener" + } + + const win = window.open(`resources/navigate-require-corp.sub.html?channelName=${bc.name}${clearOpener}&to=navigate-none.sub.html?channelName=${bc2.name}`, "_blank", noopener); + if (name === "noopener") { + assert_equals(win, null); + } else { + t.add_cleanup(() => win.close()); + } + }, `"require-corp" top-level (${title}): navigating to "none" should succeed`); +}); + +promise_test(async t => { + const response = await fetch(get_host_info().HTTPS_REMOTE_ORIGIN+"/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js", {mode: "no-cors"}); + assert_equals(response.type, "opaque"); +}, `"require-corp" top-level: fetch() to CORP: cross-origin response should succeed`); + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch(get_host_info().HTTPS_REMOTE_ORIGIN+"/common/blank.html", {mode: "no-cors"})); +}, `"require-corp" top-level: fetch() to response without CORP should fail`); + +promise_test(t => { + const w = window.open(); + return promise_rejects_js(t, w.TypeError, w.fetch(get_host_info().HTTPS_REMOTE_ORIGIN+"/common/blank.html", {mode: "no-cors"})); +}, `"require-corp" top-level: fetch() to response without CORP through a WindowProxy should fail`); + +async_test(t => { + let w = window.open(); + const frame = w.document.createElement("iframe"); + t.add_cleanup(() => { + w.close(); + frame.remove(); + }); + frame.src = "/common/blank.html"; + document.body.append(frame); + // Make sure the iframe didn't load. See https://github.com/whatwg/html/issues/125 for why a + // timeout is used here. Long term all network error handling should be similar and have a + // reliable event. + assert_equals(frame.contentDocument.body.localName, "body"); + t.step_wait_func_done(() => frame.contentDocument === null); +}, `"require-corp" top-level: navigating an iframe to a page without CORP, through a WindowProxy, should fail`); + +async_test(t => { + const frame = document.createElement("iframe"); + const id = token(); + t.add_cleanup(() => frame.remove()); + window.addEventListener('message', t.step_func((e) => { + if (e.data === id) { + // Loaded! + t.done(); + } + })); + // REMOTE_ORIGIN is cross-origin, same-site. + frame.src = `${HOST.HTTPS_REMOTE_ORIGIN}${BASE}/navigate-require-corp-same-site.sub.html?token=${id}`; + document.body.append(frame); +}, 'CORP: same-site is checked and allowed.'); + +async_test(t => { + const frame = document.createElement("iframe"); + const id = token(); + t.add_cleanup(() => frame.remove()); + let loaded = false; + window.addEventListener('message', t.step_func((e) => { + if (e.data === id) { + loaded = true; + } + })); + t.step_timeout(() => { + // Make sure the iframe didn't load. See https://github.com/whatwg/html/issues/125 for why a + // timeout is used here. Long term all network error handling should be similar and have a + // reliable event. + assert_false(loaded); + t.done(); + }, 2000); + + // NOTESAMESITE_ORIGIN is cross-origin, cross-site. + frame.src = `${HOST.HTTPS_NOTSAMESITE_ORIGIN}${BASE}/navigate-require-corp-same-site.sub.html?token=${id}`; + document.body.append(frame); +}, 'CORP: same-site is checked and blocked.'); + +async_test(t => { + const frame = document.createElement("iframe"); + const bc = new BroadcastChannel(token()); + t.add_cleanup(() => frame.remove()); + bc.onmessage = t.step_func_done((event) => { + const payload = event.data; + assert_equals(payload, "loaded"); + }); + + const dest = `${HOST.ORIGIN}${BASE}/navigate-require-corp.sub.html?channelName=${bc.name}`; + // REMOTE_ORIGIN is cross-origin, same-site. + frame.src = `${HOST.REMOTE_ORIGIN}${BASE}/navigate-require-corp-same-site.sub.html?to=${encodeURIComponent(dest)}`; + document.body.append(frame); +}, 'navigation CORP is checked with the parent frame, not the navigation source - to be allowed'); + +async_test(t => { + const frame = document.createElement("iframe"); + const bc = new BroadcastChannel(token()); + t.add_cleanup(() => frame.remove()); + let loaded = false; + bc.onmessage = t.step_func((event) => { + loaded = true; + }); + t.step_timeout(() => { + // Make sure the iframe didn't load. See https://github.com/whatwg/html/issues/125 for why a + // timeout is used here. Long term all network error handling should be similar and have a + // reliable event. + assert_false(loaded); + t.done(); + }, 2000); + + const dest = `${HOST.REMOTE_ORIGIN}${BASE}/navigate-require-corp.sub.html?channelName=${bc.name}`; + // REMOTE_ORIGIN is cross-origin, same-site. + frame.src = `${HOST.REMOTE_ORIGIN}${BASE}/navigate-require-corp-same-site.sub.html?to=${encodeURIComponent(dest)}`; + document.body.append(frame); +}, 'navigation CORP is checked with the parent frame, not the navigation source - to be blocked'); + +async_test(t => { + const bc = new BroadcastChannel(token()); + let loaded = false; + bc.onmessage = t.step_func((event) => { + loaded = true; + }); + t.step_timeout(() => { + // Make sure the iframe didn't load. See + // https://github.com/whatwg/html/issues/125 for why a timeout is used + // here. Long term all network error handling should be similar and have a + // reliable event. + assert_false(loaded); + t.done(); + }, 2000); + + const dest = `${HOST.ORIGIN}${BASE}/navigate-require-corp.sub.html?channelName=${bc.name}`; + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + // |dest| is a same-origin URL and hence not blocked by CORP but reidirect.py + // is a cross-origin (actually cross-site) URL, so blocked by CORP. + frame.src = `${HOST.HTTPS_NOTSAMESITE_ORIGIN}/common/redirect.py?location=${encodeURIComponent(dest)}`; + document.body.append(frame); +}, 'navigation CORP is checked for each redirect'); + +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/require-corp.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html new file mode 100644 index 0000000000..928d404672 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html @@ -0,0 +1,24 @@ +<body> +<script src="script-factory.js"></script> +<script> +const query = new URLSearchParams(window.location.search); +const id = query.get("id"); +const variant = query.get("variant"); +let parent = "parent"; +if (variant === "subframe") { + parent = "parent.parent"; +} else if (variant === "popup") { + parent = "opener.parent"; +} +const blob = new Blob([`<script>${createScript(window.origin, query.get("crossOrigin"), parent, id)}<\/script>`], { type: "text/html" }); +const blobURL = URL.createObjectURL(blob); +if (variant === "subframe") { + const frame = document.createElement("iframe"); + frame.src = blobURL; + document.body.append(frame); +} else if (variant === "popup") { + window.open(blobURL); +} else { + window.location = blobURL; +} +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html.headers new file mode 100644 index 0000000000..4e798cd9f5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/blob-url-factory.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: cross-origin diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/cache-storage-reporting.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/cache-storage-reporting.js new file mode 100644 index 0000000000..86dff9c845 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/cache-storage-reporting.js @@ -0,0 +1,63 @@ +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN).href; +} + +function local(path) { + return new URL(path, location.origin).href; +} + +function encode(url) { + return encodeURI(url).replace(/\;/g, '%3B'); +} + +const resource_path = (new URL('./resources', location)).pathname; +const header_coep = '|header(Cross-Origin-Embedder-Policy,require-corp)'; +const header_coep_report_only = + '|header(Cross-Origin-Embedder-Policy-Report-Only,require-corp)'; + +const iframe_path = resource_path + '/iframe.html?pipe='; +const worker_path = resource_path + '/reporting-worker.js?pipe='; +const image_url = remote('/images/blue.png'); + +// This script attempt to load a COEP:require-corp CORP:undefined response from +// the CacheStorage. +// +// Executed from different context: +// - A Document +// - A ServiceWorker +// - A DedicatedWorker +// - A SharedWorker +// +// The context has either COEP or COEP-Report-Only defined. +const eval_script = ` + (async function() { + try { + const cache = await caches.open('v1'); + const request = new Request('${image_url}', { mode: 'no-cors' }); + const response = await cache.match(request); + } catch(e) { + } + })() +`; + +promise_setup(async (t) => { + const cache = await caches.open('v1'); + const request = new Request(image_url, {mode: 'no-cors'}); + const response = await fetch(request); + await cache.put(request, response); +}, 'Setup: store a CORS:cross-origin COEP:none response into CacheStorage') + +async function makeIframe(test, iframe_url) { + const iframe = document.createElement('iframe'); + test.add_cleanup(() => iframe.remove()); + iframe.src = iframe_url; + const iframe_loaded = new Promise(resolve => iframe.onload = resolve); + document.body.appendChild(iframe); + await iframe_loaded; + return iframe; +} + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html new file mode 100644 index 0000000000..78c1331132 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html @@ -0,0 +1,2 @@ +<!doctype html> +<p><code>Cross-Origin-Embedder-Policy: require-corp</code> header set. diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/coep-frame.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/common.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/common.js new file mode 100644 index 0000000000..8f038a7278 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/common.js @@ -0,0 +1,19 @@ +async function createIsolatedFrame(origin, headers) { + const parent = document.createElement('iframe'); + const parent_loaded = new Promise(r => parent.onload = () => { r(parent); }); + const error = new Promise(r => parent.onerror = r); + parent.src = origin + "/common/blank.html?pipe=" + headers; + parent.anonymous = false; + document.body.appendChild(parent); + return [parent_loaded, error]; +} + +async function IsCrossOriginIsolated(from_token) { + const reply_token = token(); + send(from_token, ` + send("${reply_token}", self.crossOriginIsolated); + `); + const reply = await receive(reply_token); + assert_true(reply.match(/true|false/) != null); + return reply == 'true'; +}
\ No newline at end of file diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/corp-image.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/corp-image.py new file mode 100644 index 0000000000..e507846181 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/corp-image.py @@ -0,0 +1,31 @@ +import json +import base64 + +# A 1x1 PNG image. +# Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" + +def main(request, response): + response.headers.set(b'Access-Control-Allow-Origin', b'*') + response.headers.set(b'Access-Control-Allow-Methods', b'OPTIONS, GET, POST') + response.headers.set(b'Access-Control-Allow-Headers', b'Content-Type') + + # CORS preflight + if request.method == u'OPTIONS': + return u'' + + if b'true' == request.GET.get(b'revalidate', None): + response.headers.set(b'Cache-Control', b'max-age=0, must-revalidate') + else: + response.headers.set(b'Cache-Control', b'max-age=3600'); + + if b'some-etag' == request.headers.get(b'If-None-Match', None): + response.status = 304 + return u'' + + if request.GET.get(b'corp-cross-origin', None): + response.headers.set(b'Cross-Origin-Resource-Policy', b'cross-origin') + + response.headers.set(b'Etag', b'some-etag') + response.headers.set(b'Content-Type', b'image/png') + return base64.b64decode(IMAGE) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker-supporting-revalidation.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker-supporting-revalidation.py new file mode 100755 index 0000000000..eef86d1c55 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker-supporting-revalidation.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + + +def main(request, response): + headers = [] + if request.headers.get(b'if-none-match', None): + status = 304, u"Not Modified" + return status, headers, u"" + else: + headers.append((b"Content-Type", b"text/javascript")) + headers.append((b"Cross-Origin-Embedder-Policy", b"require-corp")) + headers.append((b"Cache-Control", b"private, max-age=0, must-revalidate")) + headers.append((b"ETag", b"abcdef")) + status = 200, u"OK" + return status, headers, u"self.onmessage = (e) => { self.postMessage('LOADED'); };" diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker.js new file mode 100644 index 0000000000..66f3cc3d41 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/dedicated-worker.js @@ -0,0 +1,7 @@ +self.onmessage = (e) => { + fetch(e.data, {mode: 'no-cors'}).then(() => { + self.postMessage('LOADED'); + }, () => { + self.postMessage('FAILED'); + }); +}; diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/empty-coep.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/empty-coep.py new file mode 100644 index 0000000000..d0e547b130 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/empty-coep.py @@ -0,0 +1,7 @@ +def main(request, response): + headers = [(b'Content-Type', b'text/html')] + + for value in request.GET.get_list(b'value'): + headers.append((b'Cross-Origin-Embedder-Policy', value)) + + return (200, headers, u'') diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-and-create-url.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-and-create-url.html new file mode 100644 index 0000000000..6b0f96221d --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-and-create-url.html @@ -0,0 +1,91 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Fetch and create Blob</title> +<script> + async function responseToBlob(response) { + let blob; + try { + blob = await response.blob(); + } catch (e) { + return { error: `blob error: ${e.name}` }; + } + + return { url: URL.createObjectURL(blob) }; + } + + async function responseToData(response) { + const mimeType = response.headers.get("content-type"); + + let text; + try { + text = await response.text(); + } catch(e) { + return { error: `text error: ${e.name}` }; + } + + return { url: `data:${mimeType},${encodeURIComponent(text)}` }; + } + + async function responseToFilesystem(response) { + if (!window.webkitRequestFileSystem) { + return { error: "unimplemented" }; + } + + let blob; + try { + blob = await response.blob(); + } catch (e) { + return { error: `blob error: ${e.name}` }; + } + + const fs = await new Promise(resolve => { + window.webkitRequestFileSystem(window.TEMPORARY, 1024*1024, resolve); + }); + + const file = await new Promise(resolve => { + fs.root.getFile('fetch-and-create-url', { create: true }, resolve); + }); + + const writer = await new Promise(resolve => file.createWriter(resolve)); + + try { + await new Promise((resolve, reject) => { + writer.onwriteend = resolve; + writer.onerror = reject; + writer.write(blob); + }); + } catch (e) { + return { error: `file write error: ${e.name}` }; + } + + return { url: file.toURL() }; + } + + async function responseToScheme(response, scheme) { + switch (scheme) { + case "blob": + return responseToBlob(response); + case "data": + return responseToData(response); + case "filesystem": + return responseToFilesystem(response); + default: + return { error: `unknown scheme: ${scheme}` }; + } + } + + async function fetchToScheme(url, scheme) { + let response; + try { + response = await fetch(url); + } catch (e) { + return { error: `fetch error: ${e.name}` }; + } + + return responseToScheme(response, scheme); + } + + const params = new URL(window.location).searchParams; + fetchToScheme(params.get("url"), params.get("scheme")) + .then((message) => { parent.postMessage(message, "*"); }); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-in-dedicated-worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-in-dedicated-worker.js new file mode 100644 index 0000000000..bd60d07952 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/fetch-in-dedicated-worker.js @@ -0,0 +1,6 @@ +self.addEventListener('message', async (e) => { + const param = e.data; + // Ignore network error. + await fetch(param.url, param.init).catch(() => {}); + self.postMessage(param.url); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/iframe.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/iframe.html new file mode 100644 index 0000000000..a6b74ad924 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/iframe.html @@ -0,0 +1,3 @@ +<script> + window.addEventListener("message", message => eval(message.data)); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html new file mode 100644 index 0000000000..288610046e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html @@ -0,0 +1,38 @@ +<!doctype html> +<html> +<script src="/common/get-host-info.sub.js"></script> +<script> + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +let params = new URLSearchParams(location.search); +let token = params.get('token'); +let revalidate = params.get('revalidate'); + +let image_path = `/html/cross-origin-embedder-policy/resources/corp-image.py?token=${token}&revalidate=${revalidate}`; + +window.addEventListener("load", async () => { + await new Promise(resolve => { + let img = document.createElement("img"); + img.src = remote(image_path); + img.onload = () => { window.parent.postMessage({corp: false, loaded: true}, "*"); resolve(); }; + img.onerror = (e) => { window.parent.postMessage({corp: false, loaded: false}, "*"); resolve(); }; + document.body.appendChild(img); + }); + + await new Promise(resolve => { + let img = document.createElement("img"); + img.src = remote(image_path + "&corp-cross-origin=1"); + img.onload = () => { window.parent.postMessage({corp: true, loaded: true}, "*"); resolve(); }; + img.onerror = (e) => { window.parent.postMessage({corp: true, loaded: false}, "*"); resolve(); }; + document.body.appendChild(img); + }); + + window.parent.postMessage({done: true}, "*") +}); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html.headers new file mode 100644 index 0000000000..8df98474b5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/load-corp-images.html.headers @@ -0,0 +1 @@ +cross-origin-embedder-policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-none.sub.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-none.sub.html new file mode 100644 index 0000000000..f1437ba90a --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-none.sub.html @@ -0,0 +1,34 @@ +<!doctype html> +<script> + let current = new URL(window.location.href); + let navigateTo = current.searchParams.get("to"); + let channelName = current.searchParams.get("channelName"); + let postMessageTo = current.searchParams.get("postMessageTo"); + current.search = ""; + if (navigateTo) { + let next = new URL(navigateTo, current); + window.addEventListener("load", () => { + window.location.href = next.href; + }); + } + + let target = undefined; + if (channelName) { + target = new BroadcastChannel(channelName); + } else if (postMessageTo) { + target = eval(postMessageTo); + } + + if (target) { + // Broadcast only once the DOM is loaded, so that the caller can + // access reliably this document's body. + window.addEventListener("DOMContentLoaded", () => + target.postMessage("loaded", "*")); + + // The page can also be restored from the back-forward cache: + window.addEventListener('pageshow', function(event) { + if (event.persisted) + target.postMessage("loaded", "*"); + }); + } +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html new file mode 100644 index 0000000000..910317d29b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html @@ -0,0 +1,29 @@ +<!doctype html> +<script> + const current = new URL(window.location.href); + const token = current.searchParams.get("token"); + const navigateTo = current.searchParams.get("to"); + const channelName = current.searchParams.get("channelName"); + const clearOpener = current.searchParams.get("clearOpener"); + + if (clearOpener) { + window.opener = null; + } + + current.search = ""; + if (navigateTo) { + let next = new URL(navigateTo, current); + window.addEventListener("load", () => { + window.location = next.href; + }); + } + + if (channelName) { + let bc = new BroadcastChannel(channelName); + bc.postMessage("loaded"); + } + + if (parent !== window && token) { + parent.postMessage(token, "*"); + } +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html.headers new file mode 100644 index 0000000000..56d0ac3428 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp-same-site.sub.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-site diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html new file mode 100644 index 0000000000..0a3c4dd8d7 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html @@ -0,0 +1,24 @@ +<!doctype html> +<script> + let current = new URL(window.location.href); + let navigateTo = current.searchParams.get("to"); + let channelName = current.searchParams.get("channelName"); + let clearOpener = current.searchParams.get("clearOpener"); + + if (clearOpener) { + window.opener = null; + } + + current.search = ""; + if (navigateTo) { + let next = new URL(navigateTo, current); + window.addEventListener("load", () => { + window.location.href = next.href; + }); + } + + if (channelName) { + let bc = new BroadcastChannel(channelName); + bc.postMessage("loaded"); + } +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/navigate-require-corp.sub.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js new file mode 100644 index 0000000000..662e9364f9 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js @@ -0,0 +1 @@ +/* Just an empty JS file */ diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js.headers new file mode 100644 index 0000000000..1b88136c01 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-cross-origin-corp.js.headers @@ -0,0 +1 @@ +Cross-Origin-Resource-Policy: cross-origin diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt new file mode 100644 index 0000000000..b9ba801f78 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt @@ -0,0 +1 @@ +nothing with same-origin CORP diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt.headers new file mode 100644 index 0000000000..30ddeac2e7 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/nothing-same-origin-corp.txt.headers @@ -0,0 +1 @@ +Cross-Origin-Resource-Policy: same-origin diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/postmessage-ready.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/postmessage-ready.html new file mode 100644 index 0000000000..3282711dbc --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/postmessage-ready.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + opener.postMessage("ready", "*"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/report.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/report.py new file mode 100644 index 0000000000..100c642d6c --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/report.py @@ -0,0 +1,41 @@ +import json + +def main(request, response): + response.headers.set(b'Access-Control-Allow-Origin', b'*') + response.headers.set(b'Access-Control-Allow-Methods', b'OPTIONS, GET, POST') + response.headers.set(b'Access-Control-Allow-Headers', b'Content-Type') + response.headers.set(b'Cache-Control', b'no-cache, no-store, must-revalidate') + + # CORS preflight + if request.method == u'OPTIONS': + return u'' + + uuidMap = { + b'endpoint': b'01234567-0123-0123-0123-0123456789AB', + b'report-only-endpoint': b'01234567-0123-0123-0123-0123456789CD' + } + key = 0 + if b'endpoint' in request.GET: + key = uuidMap.get(request.GET[b'endpoint'], 0) + + if b'key' in request.GET: + key = request.GET[b'key'] + + if key == 0: + response.status = 400 + return u'invalid endpoint' + + path = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + if request.method == u'POST': + reports = request.server.stash.take(key, path) or [] + for report in json.loads(request.body): + reports.append(report) + request.server.stash.put(key, reports, path) + return u'done' + + if request.method == u'GET': + response.headers.set(b'Content-Type', b'application/json') + return json.dumps(request.server.stash.take(key, path) or []) + + response.status = 400 + return u'invalid method' diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame-multiple-headers.html.asis b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame-multiple-headers.html.asis new file mode 100644 index 0000000000..c0b352f1c7 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame-multiple-headers.html.asis @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-Type: text/html +cross-origin-embedder-policy: require-corp; foo=" +cross-origin-embedder-policy: "; report-to="endpoint" +cross-origin-embedder-policy-report-only: require-corp; foo=" +cross-origin-embedder-policy-report-only: "; report-to="report-only-endpoint" + +<!doctype html> +<meta charset="utf-8"> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame.html new file mode 100644 index 0000000000..b1579add2e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-empty-frame.html @@ -0,0 +1,5 @@ +<!doctype html> +<html> +<body> +</body> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-worker.js new file mode 100644 index 0000000000..0f8a2ce4c8 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/reporting-worker.js @@ -0,0 +1,25 @@ +function run({script, port}) { + const reports = []; + const observer = new ReportingObserver((rs) => { + for (const r of rs) { + reports.push(r.toJSON()); + } + }); + // Wait 200ms for reports to settle. + setTimeout(() => { + observer.disconnect(); + port.postMessage(reports); + }, 200); + observer.observe(); + + // This eval call may generate some reports. + eval(script); +} + +// For DedicatedWorker and ServiceWorker +self.addEventListener('message', (e) => run(e.data)); + +// For SharedWorker +self.addEventListener('connect', (e) => { + e.ports[0].onmessage = (ev) => run(ev.data); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js new file mode 100644 index 0000000000..e652c5bf30 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js @@ -0,0 +1,23 @@ +// Service worker with 'COEP: require-corp' response header. +// This service worker issues a network request to import scripts with or +// without CORP response header. + +importScripts("/common/get-host-info.sub.js"); + +function url_for_empty_js(corp) { + const url = new URL(get_host_info().HTTPS_REMOTE_ORIGIN); + url.pathname = '/service-workers/service-worker/resources/empty.js'; + if (corp) { + url.searchParams.set( + 'pipe', `header(Cross-Origin-Resource-Policy, ${corp})`); + } + return url.href; +} + +const params = new URL(location.href).searchParams; + +if (params.get('corp') === 'cross-origin') { + importScripts(url_for_empty_js('cross-origin')); +} else { + importScripts(url_for_empty_js()); +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw-import-scripts.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js new file mode 100644 index 0000000000..fedecee9ea --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js @@ -0,0 +1,27 @@ +// Service worker with 'COEP: require-corp' response header. +// This service worker issues a network request for a resource with or without +// CORP response header. + +importScripts("/common/get-host-info.sub.js"); + +self.addEventListener('message', e => { + e.waitUntil((async () => { + let result; + try { + let url; + if (e.data === 'WithCorp') { + url = get_host_info().HTTPS_REMOTE_ORIGIN + + '/html/cross-origin-embedder-policy/resources/' + + 'nothing-cross-origin-corp.js'; + } else if (e.data === 'WithoutCorp') { + url = get_host_info().HTTPS_REMOTE_ORIGIN + '/common/blank.html'; + } + const response = await fetch(url, { mode: 'no-cors' }); + result = response.type; + } catch (error) { + result = `Exception: ${error.name}`; + } finally { + e.source.postMessage(result); + } + })()); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/require-corp-sw.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/script-factory.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/script-factory.js new file mode 100644 index 0000000000..ac7a1fda06 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/script-factory.js @@ -0,0 +1,30 @@ +// This creates a serialized <script> element that is useful for blob/data/srcdoc-style tests. + +function createScript(sameOrigin, crossOrigin, type="parent", id="") { + return `const data = { id: "${id}", + opener: !!window.opener, + origin: window.origin, + sameOriginNoCORPSuccess: false, + crossOriginNoCORPFailure: false }; +function record(promise, token, expectation) { + return promise.then(() => data[token] = expectation, () => data[token] = !expectation); +} + +const records = [ + record(fetch("${crossOrigin}/common/blank.html", { mode: "no-cors" }), "crossOriginNoCORPFailure", false) +]; + +if ("${sameOrigin}" !== "null") { + records.push(record(fetch("${sameOrigin}/common/blank.html", { mode: "no-cors" }), "sameOriginNoCORPSuccess", true)); +} + +Promise.all(records).then(() => { + // Using BroadcastChannel is useful for blob: URLs, which are always same-origin + if ("${type}" === "channel") { + const bc = new BroadcastChannel("${id}"); + bc.postMessage(data); + } else { + window.${type}.postMessage(data, "*"); + } +});`; +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker-fetch.js.py b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker-fetch.js.py new file mode 100644 index 0000000000..112d7ecbeb --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker-fetch.js.py @@ -0,0 +1,24 @@ +body = b''' +'use strict'; + +onconnect = (event) => { + const port = event.ports[0]; + + port.onmessage = (event) => { + fetch(event.data, { mode: 'no-cors' }) + .then( + () => port.postMessage('success'), + () => port.postMessage('failure') + ); + }; + + port.postMessage('ready'); +};''' + +def main(request, response): + headers = [(b'Content-Type', b'text/javascript')] + + for value in request.GET.get_list(b'value'): + headers.append((b'Cross-Origin-Embedder-Policy', value)) + + return (200, headers, body) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker.js new file mode 100644 index 0000000000..c5f2c3cc2c --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/shared-worker.js @@ -0,0 +1,7 @@ +onconnect = (event) => { + const port = event.ports[0]; + port.onmessage = (event) => { + eval(event.data); + }; + port.postMessage('ready'); +}; diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw-store-to-cache-storage.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw-store-to-cache-storage.js new file mode 100644 index 0000000000..00b9e9395a --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw-store-to-cache-storage.js @@ -0,0 +1,31 @@ +self.addEventListener('activate', (e) => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('message', (e) => { + e.waitUntil((async () => { + + const url = new URL(e.data.url); + const request = new Request(url, {mode: e.data.mode}); + const cache = await caches.open('v1'); + + let response; + switch(e.data.source) { + case "service-worker": + response = new Response('foo'); + break; + + case "network": + try { + response = await fetch(request); + } catch(error) { + e.source.postMessage('not-stored'); + return; + } + break; + } + + await cache.put(request, response); + e.source.postMessage('stored'); + })()); +}) diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw.js new file mode 100644 index 0000000000..57f0b41ba5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/sw.js @@ -0,0 +1,12 @@ +self.addEventListener('activate', (e) => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('fetch', (e) => { + const url = new URL(e.request.url); + if (url.searchParams.has('passthrough')) { + return; + } + + e.respondWith(fetch(e.request)); +}); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/universal-worker.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/universal-worker.js new file mode 100644 index 0000000000..5d46edcde2 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/universal-worker.js @@ -0,0 +1 @@ +onmessage = message => eval(message.data); diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner-frame.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner-frame.html new file mode 100644 index 0000000000..509c904d2b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner-frame.html @@ -0,0 +1,2 @@ +<!doctype html> +<script src="worker-owner.js"></script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner.js new file mode 100644 index 0000000000..d1f172a0b8 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-owner.js @@ -0,0 +1,36 @@ +const is_worker = !('window' in self); +const parent_or_self = is_worker ? self : self.parent; + +function startWorkerAndObserveReports(worker_url, wait_for_report) { + const worker = new Worker(worker_url); + const result_promise = new Promise(resolve => { + worker.onmessage = _ => resolve('success'); + worker.onerror = _ => resolve('error'); + }); + worker.postMessage("postMessage('reply to owner from worker');"); + + const report_promise = new Promise(resolve => { + const observer = new ReportingObserver(reports => { + observer.disconnect(); + resolve(reports.map(r => r.toJSON())); + }); + observer.observe(); + }); + + if (wait_for_report) { + Promise.all([result_promise, report_promise]).then(results => { + parent_or_self.postMessage(results[1]); + }); + } else { + result_promise.then(result => { + parent_or_self.postMessage([]); + }); + } +} + +if (is_worker) { + onmessage = e => { + startWorkerAndObserveReports(e.data.worker_url, e.data.wait_for_report); + }; +} + diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-support.js b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-support.js new file mode 100644 index 0000000000..860ee6826c --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/resources/worker-support.js @@ -0,0 +1,81 @@ +// Configures `url` such that the response carries a `COEP: ${value}` header. +// +// `url` must be a `URL` instance. +function setCoep(url, value) { + url.searchParams + .set("pipe", `header(cross-origin-embedder-policy,${value})`); +} + +// Resolves the given `relativeUrl` relative to the current window's location. +// +// `options` can contain the following keys: +// +// - `coep`: value passed to `setCoep()`, if present. +// - `host`: overrides the host of the returned URL. +// +// Returns a `URL` instance. +function resolveUrl(relativeUrl, options) { + const url = new URL(relativeUrl, window.location); + + if (options !== undefined) { + const { coep, host } = options; + if (coep !== undefined) { + setCoep(url, coep); + } + if (host !== undefined) { + url.host = host; + } + } + + return url; +} + +// Adds an iframe loaded from `url` to the current document, waiting for it to +// load before returning. +// +// The returned iframe is removed from the document at the end of the test `t`. +async function withIframe(t, url) { + const frame = document.createElement("iframe"); + frame.src = url; + + t.add_cleanup(() => frame.remove()); + + const loadedPromise = new Promise(resolve => { + frame.addEventListener('load', resolve, {once: true}); + }); + document.body.append(frame); + await loadedPromise; + + return frame; +} + +// Asynchronously waits for a single "message" event on the given `target`. +function waitForMessage(target) { + return new Promise(resolve => { + target.addEventListener('message', resolve, {once: true}); + }); +} + +// Fetches `url` from a document with COEP `creatorCoep`, then serializes it +// and returns a URL pointing to the fetched body with the given `scheme`. +// +// - `creatorCoep` is passed to `setCoep()`. +// - `scheme` may be one of: "blob", "data" or "filesystem". +// +// The returned URL is valid until the end of the test `t`. +async function createLocalUrl(t, { url, creatorCoep, scheme }) { + const frameUrl = resolveUrl("resources/fetch-and-create-url.html", { + coep: creatorCoep, + }); + frameUrl.searchParams.set("url", url); + frameUrl.searchParams.set("scheme", scheme); + + const messagePromise = waitForMessage(window); + const frame = await withIframe(t, frameUrl); + + const evt = await messagePromise; + const message = evt.data; + assert_equals(message.error, undefined, "url creation error"); + + return message.url; +} diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html new file mode 100644 index 0000000000..7a29ac1944 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html @@ -0,0 +1,40 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<div id=log></div> +<script> +async_test(t => { + window.addEventListener("message", t.step_func_done(({ data }) => { + assert_equals(data.origin, "null"); + assert_true(data.sameOriginWithoutCORP, "Request to same-origin resource without CORP did not fail"); + assert_true(data.sameOriginWithSameOriginCORP, "Request to same-origin resource with same-origin CORP did not fail"); + assert_true(data.sameOriginWithCrossOriginCORP, "Request to same-origin resource with cross-origin CORP did not succeed"); + assert_true(data.crossOriginWithCrossOriginCORP, "Request to cross-origin resource with cross-origin CORP did not succeed"); + })); + + const origins = get_host_info(); + const frame = document.createElement("iframe"); + const nothingCrossOriginCORP = new URL("resources/nothing-cross-origin-corp.js", window.location).pathname; + const nothingSameOriginCORP = new URL("resources/nothing-same-origin-corp.txt", window.location).pathname; + frame.sandbox = "allow-scripts"; + frame.srcdoc = `<script> +const data = { sameOriginWithoutCORP: false, + sameOriginWithSameOriginCORP: false, + sameOriginWithCrossOriginCORP: false, + crossOriginWithCrossOriginCORP: false, + origin: self.origin }; +function record(promise, token, expectation) { + return promise.then(() => data[token] = expectation, () => data[token] = !expectation); +} +Promise.all([ + record(fetch("/common/blank.html", { mode: "no-cors" }), "sameOriginWithoutCORP", false), + record(fetch("${nothingSameOriginCORP}", { mode: "no-cors" }), "sameOriginWithSameOriginCORP", false), + record(fetch("${nothingCrossOriginCORP}", { mode: "no-cors" }), "sameOriginWithCrossOriginCORP", true), + record(fetch("${origins.HTTPS_NOTSAMESITE_ORIGIN}${nothingCrossOriginCORP}", { mode: "no-cors" }), "crossOriginWithCrossOriginCORP", true) +]).then(() => parent.postMessage(data, "*")); +<\/script>`; + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy and sandbox"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/sandbox.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/service-worker-cache-storage.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/service-worker-cache-storage.https.html new file mode 100644 index 0000000000..873f06ce4f --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/service-worker-cache-storage.https.html @@ -0,0 +1,117 @@ +<!doctype html> +<html> +<title> Check enforcement of COEP in a ServiceWorker using CacheStorage. </title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +// See also: ./dedicated-worker-cache-storage.https.html + +function remote(path) { + const REMOTE_ORIGIN = get_host_info().HTTPS_REMOTE_ORIGIN; + return new URL(path, REMOTE_ORIGIN); +} + +const iframe_path = "./resources/iframe.html?pipe="; +const service_worker_path = "./resources/universal-worker.js?pipe="; +const ressource_path = "/images/blue.png?pipe="; + +const coep_header= { + "coep-none" : "", + "coep-require-corp" : "|header(Cross-Origin-Embedder-Policy,require-corp)", +} + +const corp_header = { + "corp-undefined" : "", + "corp-cross-origin" : "|header(Cross-Origin-Resource-Policy,cross-origin)", +} + +// Send a message to the |worker| and wait for its response. +function executeCommandInServiceWorker(worker, command) { + const channel = new MessageChannel(); + const response = new Promise(resolve => channel.port1.onmessage = resolve); + worker.postMessage(command, [ channel.port2 ]); + return response; +} + +// Check enforcement of COEP in a ServiceWorker using CacheStorage. +// +// 1) Fetch a response from a document with COEP:none. Store it in the +// CacheStorage. The response is cross-origin without any CORS header. +// 2) From a ServiceWorker, retrieve the response from the CacheStorage. +// +// Test parameters: +// - |worker_coep| the COEP header of the ServiceWorker's script response. +// - |response_corp| the CORP header of the response. +// +// Test expectations: +// |loaded| is true whenever the worker is able to fetch the response from +// the CacheStorage. According to the specification: +// https://mikewest.github.io/corpp/#initialize-embedder-policy-for-global +// it must be false when: +// - |worker_coep| is 'coep-require-corp' and +// - |response-corp| is 'corp-undefined'. +function check( + // Test parameters: + worker_coep, + response_corp, + + // Test expectations: + loaded) { + + promise_test(async (t) => { + // 1) Fetch a response from a document with COEP:none. Store it in the + // CacheStorage. The response is cross-origin without any CORS header. + const resource_path = ressource_path + corp_header[response_corp]; + const resource_url = remote(resource_path); + const fetch_request = new Request(resource_url, {mode: 'no-cors'}); + const cache = await caches.open('v1'); + const fetch_response = await fetch(fetch_request); + await cache.put(fetch_request, fetch_response); + + // 2) Start a ServiceWorker. + const SCOPE= new URL(location.href).pathname; + const service_worker_allowed = `|header(service-worker-allowed,${SCOPE})`; + const SCRIPT = + service_worker_path + + coep_header[worker_coep] + + service_worker_allowed; + + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + add_completion_callback(() => reg.unregister()); + + // Start talking to the ServiceWorker, no matter its state. + const worker = reg.installing || reg.waiting || reg.active; + + // 3) From the service worker, try to retrieve the response from the + // CacheStorage. + const response = executeCommandInServiceWorker(worker, ` + (async function() { + const cache = await caches.open('v1'); + const request = new Request('${resource_url}', { + mode: 'no-cors' + }); + try { + const response = await cache.match(request); + message.ports[0].postMessage('success'); + } catch(error) { + message.ports[0].postMessage('error'); + } + })() + `); + const {data} = await response; + assert_equals(data === "success", loaded); + }, `A ServiceWorker with ${worker_coep} use CacheStorage to get a ${response_corp} response.`) +} + +// ------------------------------------------------------ +// worker_coep , response_corp , loaded +// ------------------------------------------------------ +check("coep-none" , "corp-undefined" , true); +check("coep-none" , "corp-cross-origin" , true); +check("coep-require-corp" , "corp-undefined" , false); +check("coep-require-corp" , "corp-cross-origin" , true); + +</script> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/shared-workers.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/shared-workers.https.html new file mode 100644 index 0000000000..2558f2dd0b --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/shared-workers.https.html @@ -0,0 +1,228 @@ +<!doctype html> +<html> +<meta charset="utf-8"> +<title>COEP - policy derivation for Shared Workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="resources/worker-support.js"></script> +<body> +<p>Verify the Cross-Origin Embedder Policy for Shared Workers by performing a +cross-domain "fetch" request for a resource that does not specify a COEP. Only +Shared Workers with the default COEP should be able to successfully perform +this operation.</p> +<script> +'use strict'; + +const testUrl = resolveUrl("resources/empty-coep.py", { + host: get_host_info().REMOTE_HOST, +}).href; + +function makeWorkerUrl(options) { + return resolveUrl("resources/shared-worker-fetch.js.py", options); +} + +/** + * Create a Shared Worker within an iframe + * + * @param {object} t - a testharness.js subtest instance (used to reset global + * state) + * @param {string} url - the URL from which the Shared Worker should be + * created + * @param {string} options.ownerCoep - the Cross-Origin Embedder Policy of the + iframe + */ +async function createWorker(t, url, options) { + const { ownerCoep } = options || {}; + const frameUrl = resolveUrl("/common/blank.html", { coep: ownerCoep }); + + const iframe = await withIframe(t, frameUrl); + + const sw = new iframe.contentWindow.SharedWorker(url); + sw.onerror = t.unreached_func('SharedWorker.onerror should not be called'); + + await new Promise((resolve) => { + sw.port.addEventListener('message', resolve, { once: true }); + sw.port.start(); + }); + + return sw; +} + +/** + * Instruct a Shared Worker to fetch from a specified URL and report on the + * success of the operation. + * + * @param {SharedWorker} worker + * @param {string} url - the URL that the worker should fetch + */ +function fetchFromWorker(worker, url) { + return new Promise((resolve) => { + worker.port.postMessage(url); + worker.port.addEventListener( + 'message', (event) => resolve(event.data), { once: true } + ); + }); +}; + +promise_test(async (t) => { + const worker = await createWorker(t, makeWorkerUrl()); + const result = await fetchFromWorker(worker, testUrl); + assert_equals(result, 'success'); +}, 'default policy (derived from response)'); + +promise_test(async (t) => { + const worker = await createWorker(t, makeWorkerUrl({ coep: 'require-corp' })); + const result = await fetchFromWorker(worker, testUrl); + assert_equals(result, 'failure'); +}, '"require-corp" (derived from response)'); + +promise_test(async (t) => { + const blobUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "blob", + }); + + const workers = await Promise.all([ + createWorker(t, blobUrl), + createWorker(t, blobUrl), + createWorker(t, blobUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'success'); +}, 'default policy (derived from owner set due to use of local scheme - blob URL)'); + +promise_test(async (t) => { + const blobUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + creatorCoep: "require-corp", + scheme: "blob", + }); + + const workers = await Promise.all([ + createWorker(t, blobUrl), + createWorker(t, blobUrl), + createWorker(t, blobUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'failure'); +}, 'require-corp (derived from blob URL creator)'); + +promise_test(async (t) => { + const blobUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "blob", + }); + + const workers = await Promise.all([ + createWorker(t, blobUrl), + createWorker(t, blobUrl, { ownerCoep: 'require-corp' }), + createWorker(t, blobUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'failure'); +}, '"require-corp" (derived from owner set due to use of local scheme - blob URL)'); + +promise_test(async (t) => { + const dataUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "data", + }); + + const workers = await Promise.all([ + createWorker(t, dataUrl), + createWorker(t, dataUrl), + createWorker(t, dataUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'success'); +}, 'default policy (derived from owner set due to use of local scheme - data URL)'); + +promise_test(async (t) => { + const dataUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + creatorCoep: "require-corp", + scheme: "data", + }); + + const workers = await Promise.all([ + createWorker(t, dataUrl), + createWorker(t, dataUrl), + createWorker(t, dataUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'success'); +}, 'default policy (not derived from data URL creator)'); + +promise_test(async (t) => { + const dataUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "data", + }); + + const workers = await Promise.all([ + createWorker(t, dataUrl), + createWorker(t, dataUrl, { ownercoep: 'require-corp' }), + createWorker(t, dataUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'failure'); +}, '"require-corp" (derived from owner set due to use of local scheme - data URL)'); + +promise_test(async (t) => { + const filesystemUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "filesystem", + }); + + const workers = await Promise.all([ + createWorker(t, filesystemUrl), + createWorker(t, filesystemUrl), + createWorker(t, filesystemUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'success'); +}, 'default policy (derived from owner set due to use of local scheme - filesystem URL)'); + +promise_test(async (t) => { + const filesystemUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + creatorCoep: "require-corp", + scheme: "filesystem", + }); + + const workers = await Promise.all([ + createWorker(t, filesystemUrl), + createWorker(t, filesystemUrl), + createWorker(t, filesystemUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'failure'); +}, 'require-corp (derived from filesystem URL creator)'); + +promise_test(async (t) => { + const filesystemUrl = await createLocalUrl(t, { + url: makeWorkerUrl(), + scheme: "filesystem", + }); + + const workers = await Promise.all([ + createWorker(t, filesystemUrl), + createWorker(t, filesystemUrl, { ownerCoep: 'require-corp' }), + createWorker(t, filesystemUrl), + ]); + + const result = await fetchFromWorker(workers[0], testUrl); + assert_equals(result, 'failure'); +}, '"require-corp" (derived from owner set due to use of local scheme - filesystem URL)'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html new file mode 100644 index 0000000000..2937c13381 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html @@ -0,0 +1,21 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/script-factory.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<div id=log></div> +<script> +async_test(t => { + window.addEventListener("message", t.step_func_done(({ data }) => { + assert_equals(data.id, ""); + assert_equals(data.origin, window.origin); + assert_true(data.sameOriginNoCORPSuccess, "Same-origin without CORP did not succeed"); + assert_true(data.crossOriginNoCORPFailure, "Cross-origin without CORP did not fail"); + })); + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.srcdoc = `<script>${createScript(window.origin, get_host_info().HTTPS_NOTSAMESITE_ORIGIN)}<\/script>`; + document.body.append(frame); +}, "Cross-Origin-Embedder-Policy and srcdoc"); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/srcdoc.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html b/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html new file mode 100644 index 0000000000..e96c7f7e5d --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<title>Test that local scheme workers inherit COEP: require-corp from the creating document</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + promise_test(async t => { + let sameOrigin = "{{location[server]}}"; + let crossOrigin = "https://{{hosts[][www]}}:{{ports[https][1]}}"; + + let testHarness = await fetch(`${sameOrigin}/resources/testharness.js`) + .then(r => r.text()); + + // Test that fetching same-origin is allowed by COEP. + let same_origin_allowed_test = testName => ` + promise_test(async t => { + return fetch("${sameOrigin}/common/blank.html", { mode: "no-cors" }); + }, "${testName}: Same origin should be allowed."); + `; + + // For data URLs, since everything is cross-origin in that case. + let same_origin_blocked_test = testName => ` + promise_test(t => { + return promise_rejects_js( + t, TypeError, + fetch("${sameOrigin}/common/blank.html", { mode: "no-cors" })); + }, "${testName}: Same origin should be blocked."); + `; + + // Test that fetching cross-origin is blocked by COEP. + let cross_origin_blocked_test = testName => ` + promise_test(t => { + return promise_rejects_js( + t, TypeError, + fetch("${crossOrigin}/common/blank.html", { mode: "no-cors" })); + }, "${testName}: Cross origin should be blocked."); + `; + + let blob_string = testName => testHarness + + same_origin_allowed_test(testName) + + cross_origin_blocked_test(testName) + "done();"; + + let data_string = testName => testHarness + + same_origin_blocked_test(testName) + + cross_origin_blocked_test(testName) + "done();"; + + let blob_url = context => { + let blob = new Blob([blob_string(`blob URL ${context}`)], + { type: 'application/javascript' }); + return URL.createObjectURL(blob); + }; + + await fetch_tests_from_worker(new Worker(blob_url("dedicated worker"))); + await fetch_tests_from_worker(new SharedWorker(blob_url("shared worker"))); + + let data_url = context => `data:application/javascript,` + + `${encodeURIComponent(data_string("data URL " + context))}`; + + await fetch_tests_from_worker(new Worker(data_url("dedicated worker"))); + await fetch_tests_from_worker(new Worker(data_url("shared worker"))); + }); +</script> diff --git a/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html.headers b/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html.headers new file mode 100644 index 0000000000..6604450991 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-embedder-policy/worker-inheritance.sub.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp |