From 8cc2a714165951cbc3745cab55602a51d87689d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A2ris=20Meuleman?= Date: Mon, 8 Jun 2020 06:27:54 -0700 Subject: [PATCH] [Security][Coop] Browsing context switch reporting WPT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This CL adds basic reporting tests for browsing context switches. It provides a reporting endpoint (report.py), and reusable helpers within reporting-common.js, allowing future tests. The helpers provided verify that expected report templates are present on the expected endpoints, and that no extraneous reports are present. This CL only convers the cases: Popup opened from pages with coop : Same-origin with report only navigating to * Same-origin (without report) navigatin to *-with report Follow ups will cover redirects (moved to follow up as it had timeout issues), other origins and iframe cases. Bug: 1076456 Change-Id: I7a39d4def20692d8628ce2406569638310684f4f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2207451 Reviewed-by: Arthur Sonzogni Commit-Queue: Pâris Meuleman Auto-Submit: Pâris Meuleman Cr-Commit-Position: refs/heads/master@{#776008} --- .../reporting-coop-navigated-popup.https.html | 82 +++++++ ...oop-navigated-popup.https.html.sub.headers | 2 + ...e-origin-allow-popups-report-to.https.html | 123 ++++++++++ ...-allow-popups-report-to.https.html.headers | 3 + ...opup-same-origin-coep-report-to.https.html | 108 +++++++++ ...e-origin-coep-report-to.https.html.headers | 4 + ...ing-popup-same-origin-report-to.https.html | 213 ++++++++++++++++++ ...me-origin-report-to.https.html.sub.headers | 3 + .../reporting-popup-same-origin.https.html | 96 ++++++++ ...rting-popup-same-origin.https.html.headers | 1 + ...ting-popup-unafe-none-report-to.https.html | 123 ++++++++++ ...up-unafe-none-report-to.https.html.headers | 2 + .../resources/common.js | 17 +- .../resources/coop-coep.py | 29 +++ .../resources/report.py | 26 +++ .../resources/reporting-common.js | 177 +++++++++++++++ 16 files changed, 1002 insertions(+), 7 deletions(-) create mode 100644 html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html create mode 100644 html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html.headers create mode 100644 html/cross-origin-opener-policy/resources/report.py create mode 100644 html/cross-origin-opener-policy/resources/reporting-common.js diff --git a/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html new file mode 100644 index 00000000000000..8642a0aba710ba --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html @@ -0,0 +1,82 @@ +Cross-Origin-Opener-Policy: a navigated popup with reporting + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers new file mode 100644 index 00000000000000..b4d5d16cf8d516 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers @@ -0,0 +1,2 @@ +Cross-Origin-Opener-Policy: same-origin-allow-popups; report-to="coop-report-endpoint" +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html new file mode 100644 index 00000000000000..678fe83a9a7db8 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html @@ -0,0 +1,123 @@ + +reporting same origin with report-to + + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html.headers new file mode 100644 index 00000000000000..bcc03a6fa38bc8 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-allow-popups-report-to.https.html.headers @@ -0,0 +1,3 @@ +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} +Cross-Origin-Opener-Policy: same-origin-allow-popups; report-to="coop-report-endpoint" +Referrer-Policy: origin diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html new file mode 100644 index 00000000000000..80a7853ce88030 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html @@ -0,0 +1,108 @@ + +reporting same origin with report-to + + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html.headers new file mode 100644 index 00000000000000..fb5526fbdac60f --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-coep-report-to.https.html.headers @@ -0,0 +1,4 @@ +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} +Cross-Origin-Opener-Policy: same-origin; report-to="coop-report-endpoint" +Cross-Origin-Embedder-Policy: require-corp +Referrer-Policy: origin \ No newline at end of file diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html new file mode 100644 index 00000000000000..02f70d5458ac50 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html @@ -0,0 +1,213 @@ + +reporting same origin with report-to + + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers new file mode 100644 index 00000000000000..3850783812d3ad --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers @@ -0,0 +1,3 @@ +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} +Cross-Origin-Opener-Policy: same-origin; report-to="coop-report-endpoint" +Referrer-Policy: no-referrer diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html new file mode 100644 index 00000000000000..a2ed37e46752eb --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html @@ -0,0 +1,96 @@ + +reporting same origin + + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers new file mode 100644 index 00000000000000..46ad58d83bf6e9 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Opener-Policy: same-origin diff --git a/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html b/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html new file mode 100644 index 00000000000000..62eb006586478a --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html @@ -0,0 +1,123 @@ + +reporting same origin with report-to + + + + + + + diff --git a/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html.headers b/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html.headers new file mode 100644 index 00000000000000..d6a6e7ecdcd131 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-unafe-none-report-to.https.html.headers @@ -0,0 +1,2 @@ +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} +Cross-Origin-Opener-Policy: unsafe-none; report-to="coop-report-endpoint" diff --git a/html/cross-origin-opener-policy/resources/common.js b/html/cross-origin-opener-policy/resources/common.js index fb517e8c40ac56..8a3cd133734dc3 100644 --- a/html/cross-origin-opener-policy/resources/common.js +++ b/html/cross-origin-opener-policy/resources/common.js @@ -29,11 +29,14 @@ function validate_results(callback, test, w, channelName, hasOpener, openerDOMAc } } -function url_test(t, url, channelName, hasOpener, openerDOMAccess) { +function url_test(t, url, channelName, hasOpener, openerDOMAccess, callback) { + if (callback === undefined) { + callback = () => { t.done(); }; + } const bc = new BroadcastChannel(channelName); bc.onmessage = t.step_func(event => { const payload = event.data; - validate_results(() => { t.done(); }, t, w, channelName, hasOpener, openerDOMAccess, payload); + validate_results(callback, t, w, channelName, hasOpener, openerDOMAccess, payload); }); const w = window.open(url, channelName); @@ -46,12 +49,12 @@ function url_test(t, url, channelName, hasOpener, openerDOMAccess) { }); } -function coop_coep_test(t, host, coop, coep, channelName, hasOpener, openerDOMAccess) { - url_test(t, `${host.origin}/html/cross-origin-opener-policy/resources/coop-coep.py?coop=${encodeURIComponent(coop)}&coep=${coep}&channel=${channelName}`, channelName, hasOpener, openerDOMAccess); +function coop_coep_test(t, host, coop, coep, channelName, hasOpener, openerDOMAccess, callback) { + url_test(t, `${host.origin}/html/cross-origin-opener-policy/resources/coop-coep.py?coop=${encodeURIComponent(coop)}&coep=${coep}&channel=${channelName}`, channelName, hasOpener, openerDOMAccess, callback); } -function coop_test(t, host, coop, channelName, hasOpener) { - coop_coep_test(t, host, coop, "", channelName, hasOpener); +function coop_test(t, host, coop, channelName, hasOpener, callback) { + coop_coep_test(t, host, coop, "", channelName, hasOpener, undefined /* openerDOMAccess */, callback); } function run_coop_tests(documentCOOPValueTitle, testArray) { @@ -59,7 +62,7 @@ function run_coop_tests(documentCOOPValueTitle, testArray) { async_test(t => { coop_test(t, test[0], test[1], `${documentCOOPValueTitle}_to_${test[0].name}_${test[1].replace(/ /g,"-")}`, - test[2]); + test[2], () => { t.done(); }); }, `${documentCOOPValueTitle} document opening popup to ${test[0].origin} with COOP: "${test[1]}"`); } } diff --git a/html/cross-origin-opener-policy/resources/coop-coep.py b/html/cross-origin-opener-policy/resources/coop-coep.py index 8a7e0bc0a838fd..03fe6ccb1b96ce 100644 --- a/html/cross-origin-opener-policy/resources/coop-coep.py +++ b/html/cross-origin-opener-policy/resources/coop-coep.py @@ -1,13 +1,42 @@ +def get_reporting_group(host, endpoint): + return '\ +{{\ + "group": "{endpoint}",\ + "max_age": 10886400,\ + "endpoints":\ + [{{\ + "url": "https://{host}/html/cross-origin-opener-policy/resources/report.py?endpoint={endpoint}"\ + }}]\ +}}'.format(host=host, endpoint=endpoint) + def main(request, response): coop = request.GET.first("coop") + coopReportOnly = request.GET.first("coop-report-only") if "coop-report-only" in request.GET else "" coep = request.GET.first("coep") + coepReportOnly = request.GET.first("coep-report-only") if "coep-report-only" in request.GET else "" redirect = request.GET.first("redirect", None) if coop != "": response.headers.set("Cross-Origin-Opener-Policy", coop) + if coop != "": + response.headers.set("Cross-Origin-Opener-Policy-Report-Only", coopReportOnly) if coep != "": response.headers.set("Cross-Origin-Embedder-Policy", coep) + if coep != "": + response.headers.set("Cross-Origin-Embedder-Policy-Report-Only", coepReportOnly) if 'cache' in request.GET: response.headers.set('Cache-Control', 'max-age=3600') + host = request.url_parts[1] + + # add all possible reporting endpoints to the report-to header + # Note that this also returns the coop-report-endpoint, as it may override + # the test's endpoints if same-origin. + response.headers.set('report-to', + get_reporting_group(host, "coop-report-endpoint") + ',' + + get_reporting_group(host, "coop-report-only-endpoint") + ',' + + get_reporting_group(host, "coop-redirect-report-endpoint") + ',' + + get_reporting_group(host, "coop-redirect-report-only-endpoint") + ',' + + get_reporting_group(host, "coop-popup-report-endpoint") + ',' + + get_reporting_group(host, "coop-popup-report-only-endpoint") ) if redirect != None: response.status = 302 diff --git a/html/cross-origin-opener-policy/resources/report.py b/html/cross-origin-opener-policy/resources/report.py new file mode 100644 index 00000000000000..c9ea353a12fb18 --- /dev/null +++ b/html/cross-origin-opener-policy/resources/report.py @@ -0,0 +1,26 @@ +import json, uuid + +def main(request, response): + response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + key = 0; + if 'endpoint' in request.GET: + key = uuid.uuid5(uuid.NAMESPACE_OID, request.GET['endpoint']).get_urn() + + if key == 0: + response.status = 400 + return 'invalid endpoint' + + if request.method == 'POST': + reports = request.server.stash.take(key) or [] + for report in json.loads(request.body): + reports.append(report) + request.server.stash.put(key, reports) + return "done" + + if request.method == 'GET': + response.headers.set('Content-Type', 'application/json') + return json.dumps(request.server.stash.take(key) or []) + + response.status = 400 + return 'invalid method' diff --git a/html/cross-origin-opener-policy/resources/reporting-common.js b/html/cross-origin-opener-policy/resources/reporting-common.js new file mode 100644 index 00000000000000..d834c080e44787 --- /dev/null +++ b/html/cross-origin-opener-policy/resources/reporting-common.js @@ -0,0 +1,177 @@ +// Allows RegExps to be pretty printed when printing unmatched expected reports. +Object.defineProperty(RegExp.prototype, "toJSON", { + value: RegExp.prototype.toString +}); + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +async function pollReports(endpoint) { + const res = await fetch( + `resources/report.py?endpoint=${endpoint.name}`, + {cache: 'no-store'}); + if (res.status !== 200) { + return; + } + for (const report of await res.json()) { + endpoint.reports.push(report); + } +} + +// Recursively check that all members of expectedReport are present or matched +// in report. +// Report may have members not explicitly expected by expectedReport. +function isObjectAsExpected(report, expectedReport) { + if (( report === undefined || report === null + || expectedReport === undefined || expectedReport === null ) + && report !== expectedReport ) { + return false; + } + if (expectedReport instanceof RegExp && typeof report === "string") { + return expectedReport.test(report); + } + // Perform this check now, as RegExp and strings above have different typeof. + if (typeof report !== typeof expectedReport) + return false; + if (typeof expectedReport === 'object') { + return Object.keys(expectedReport).every(key => { + return isObjectAsExpected(report[key], expectedReport[key]); + }); + } + return report == expectedReport; +} + +async function checkForExpectedReport(expectedReport) { + return new Promise( async (resolve, reject) => { + const polls = 30; + const waitTime = 100; + for (var i=0; i < polls; ++i) { + pollReports(expectedReport.endpoint); + for (var j=0; j { + testFunction(resolve); + }); + expectedReports = Array.from( + expectedReports, + report => replaceValuesInExpectedReport(report, channelName) ); + await Promise.all(Array.from(expectedReports, checkForExpectedReport)); +} + +function coopCoepReportingTest(testName, host, coop, coep, hasOpener, + expectedReports){ + const channelName = `${testName.replace(/[ ;"=]/g,"-")}_to_${host.name}_${coop.replace(/[ ;"=]/g,"-")}_${coep}`; + promise_test(async t => { + await reportingTest( (resolve) => { + coop_coep_test(t, host, coop, coep, channelName, + hasOpener, undefined /* openerDOMAccess */, resolve); + }, channelName, expectedReports); + }, `coop reporting test ${channelName}`); +} + +// Run an array of reporting tests then verify there's no reports that were not +// expected. +// Tests' elements contain: host, coop, coep, hasOpener, expectedReports. +// See isObjectAsExpected for explanations regarding the matching behavior. +function runCoopReportingTest(testName, tests){ + tests.forEach( test => { + coopCoepReportingTest(testName, ...test); + }); + verifyRemainingReports(); +} + +const reportEndpoint = { + name: "coop-report-endpoint", + reports: [] +} +const reportOnlyEndpoint = { + name: "coop-report-only-endpoint", + reports: [] +} +const popupReportEndpoint = { + name: "coop-popup-report-endpoint", + reports: [] +} +const popupReportOnlyEndpoint = { + name: "coop-popup-report-only-endpoint", + reports: [] +} +const redirectReportEndpoint = { + name: "coop-redirect-report-endpoint", + reports: [] +} +const redirectReportOnlyEndpoint = { + name: "coop-redirect-report-only-endpoint", + reports: [] +} + +const reportEndpoints = [ + reportEndpoint, + reportOnlyEndpoint, + popupReportEndpoint, + popupReportOnlyEndpoint, + redirectReportEndpoint, + redirectReportOnlyEndpoint +] + +function verifyRemainingReports() { + promise_test( async t => { + await Promise.all(Array.from(reportEndpoints, (endpoint) => { + return new Promise( async (resolve, reject) => { + await pollReports(endpoint); + if (endpoint.reports.length != 0) + reject( `${endpoint.name} not empty`); + resolve(); + }); + })); + }, "verify remaining reports"); +}