Skip to content

Commit

Permalink
[Credentialless] Add credentialless reporting
Browse files Browse the repository at this point in the history
This CL adds support for navigational COEP:credentialless
requests to cross-origin-resource-policy reporting.
When a navigational response is blocked, COEP:credentialless
report will be sent.

Bug: 1200849
Change-Id: I3ab8235190597b6292f99afe5daffd059435d369
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2867085
Reviewed-by: Yifan Luo <[email protected]>
Reviewed-by: Arthur Sonzogni <[email protected]>
Reviewed-by: Kinuko Yasuda <[email protected]>
Commit-Queue: Yifan Luo <[email protected]>
Cr-Commit-Position: refs/heads/master@{#888454}
  • Loading branch information
iVanlIsh authored and foolip committed Jun 3, 2021
1 parent b4a1aa7 commit e3e01e7
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<!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="./resources/common.js"></script>
<script>
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) {
const reports = [];
const observer = new global.ReportingObserver((rs) => {
for (const r of rs) {
reports.push(r.toJSON());
}
});
observer.observe();

// Wait 1000ms for reports to settle.
await new Promise(r => step_timeout(r, 1000));
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);
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']);

</script>
</body></html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<!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 = "/html/cross-origin-embedder-policy/resources";
const FRAME_URL = `${ORIGIN}/common/blank.html` +
'?pipe=header(cross-origin-embedder-policy,credentialless)' +
`|header(cross-origin-embedder-policy-report-only,credentialless)`;
const WORKER_URL = `${ORIGIN}${BASE}/reporting-worker.js` +
'?pipe=header(cross-origin-embedder-policy,credentialless)' +
`|header(cross-origin-embedder-policy-report-only,credentialless)`;
const REPORTING_FRAME_URL = `${ORIGIN}${BASE}/reporting-empty-frame.html` +
'?pipe=header(cross-origin-embedder-policy,credentialless)' +
`|header(cross-origin-embedder-policy-report-only,credentialless)`;

function wait(ms) {
return new Promise(resolve => step_timeout(resolve, ms));
}

async function fetchInFrame(t, frameUrl, url) {
const reports = [];
const frame = await with_iframe(frameUrl);
t.add_cleanup(() => frame.remove());

const observer = new frame.contentWindow.ReportingObserver((rs) => {
for (const report of rs) {
reports.push(report.toJSON());
}
});
observer.observe();
const init = { mode: 'no-cors', cache: 'no-store' };
await frame.contentWindow.fetch(url, init).catch(() => {});

// Wait 1000ms for reports to settle.
await new Promise(r => step_timeout(r, 1000));
return 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 worker, 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) => {
return await fetchInFrame(test, FRAME_URL, url);
},
}, {
tag: 'dedicated worker',
contextUrl: WORKER_URL,
run: async (test, url) => {
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) => {
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) => {
// Generate a one-time scope for service workeer.
const SCOPE = `${BASE}/${token()}.html`;
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) => {
// 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);
},
}];

const CASES = [{
name: 'same-origin',
url: '/common/text-plain.txt',
check: (reports, url, contextUrl) => {
assert_equals(reports.length, 0);
}
}, {
name: 'blocked by CORP: same-origin',
url: `${REMOTE_ORIGIN}${BASE}/nothing-same-origin-corp.txt`,
check: (reports, url, contextUrl) => {
assert_equals(reports.length, 0);
}
}, {
name: 'blocked due to COEP',
url: `${REMOTE_ORIGIN}/common/text-plain.txt`,
check: (reports, contextUrl, url) => {
assert_equals(reports.length, 0);
}
}, {
name: 'blocked during redirect',
url: `${ORIGIN}/common/redirect.py?location=` +
encodeURIComponent(`${REMOTE_ORIGIN}/common/text-plain.txt`),
check: (reports, contextUrl, url) => {
assert_equals(reports.length, 0);
},
}];

for (const env of ENVIRONMENTS) {
for (const testcase of CASES) {
promise_test(async (t) => {
const reports = await env.run(t, testcase.url);
testcase.check(reports, env.contextUrl, testcase.url);
}, `[${env.tag}] ${testcase.name}`);
}
}

// A test for a non-empty destination.
promise_test(async (t) => {
const reports = [];
const frame = await with_iframe(FRAME_URL);
t.add_cleanup(() => frame.remove());

const observer = new frame.contentWindow.ReportingObserver((rs) => {
for (const report of rs) {
reports.push(report.toJSON());
}
});
observer.observe();
const url = `${REMOTE_ORIGIN}/common/utils.js`;
const script = frame.contentDocument.createElement('script');
script.src = url;
frame.contentDocument.body.appendChild(script);

// Wait 200ms for reports to settle.
await t.step_timeout(200);

assert_equals(reports.length, 0);
}, 'destination: script');

</script>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
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)`;

function wait(ms) {
return new Promise(resolve => step_timeout(resolve, ms));
Expand Down Expand Up @@ -117,18 +120,17 @@
},
}, {
tag: 'between service worker and page',
contextUrl: `${ORIGIN}${BASE}/reporting-empty-frame.html`,
contextUrl: REPORTING_FRAME_URL,
run: async (test, url) => {
const SCOPE = `${BASE}/reporting-empty-frame.html`;
// 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, SCOPE);
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, `${ORIGIN}${BASE}/reporting-empty-frame.html`, url);
test, REPORTING_FRAME_URL, url);
},
}];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
// .
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)' +
`|header(cross-origin-embedder-policy-report-only,require-corp)`;

function wait(ms) {
return new Promise(resolve => step_timeout(resolve, ms));
Expand Down Expand Up @@ -85,7 +88,7 @@
const iframe = document.createElement('iframe');
t.add_cleanup(() => iframe.remove());

iframe.src = `resources/reporting-empty-frame.html`
iframe.src = FRAME_URL
document.body.appendChild(iframe);
await new Promise(resolve => {
iframe.addEventListener('load', resolve, {once: true});
Expand All @@ -106,7 +109,7 @@
const iframe = document.createElement('iframe');
t.add_cleanup(() => iframe.remove());

iframe.src = `resources/reporting-empty-frame.html`
iframe.src = FRAME_URL
document.body.appendChild(iframe);
await new Promise(resolve => {
iframe.addEventListener('load', resolve, {once: true});
Expand Down Expand Up @@ -135,7 +138,7 @@
const iframe = document.createElement('iframe');
t.add_cleanup(() => iframe.remove());

iframe.src = 'resources/reporting-empty-frame.html';
iframe.src = FRAME_URL;
const targetUrl = `/common/blank.html?${token()}`;
iframe.addEventListener('load', t.step_func(() => {
const nested = iframe.contentDocument.createElement('iframe');
Expand Down

This file was deleted.

0 comments on commit e3e01e7

Please sign in to comment.