Skip to content

Commit

Permalink
COOP noopener-allow-popups
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=275147

Reviewed by Alex Christensen.

The `noopener-allow-popups` COOP value would enable a document to ensure it can't be scripted by other same-origin documents that have opened it.

Some origins can contain different applications with different levels of security requirements.
In those cases, it can be beneficial to prevent scripts running in one application from being able to open and script pages of another same-origin application.

The noopener-allow-popups Cross-Origin-Opener-Policy value severs the opener relationship between the document loaded with this policy and its opener.
At the same time, this document can open further documents (as the "allow-popups" in the name suggests) and maintain its opener relationship with them, assuming that their COOP policy allows it.

This implements whatwg/html#10394

* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/reporting/resources/reporting-common.js:
(const.coopHeaders): A helper to create headers in a more succinct way.
* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/reporting/tentative/access-to-noopener-page-from-no-coop-ro.https-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/reporting/tentative/access-to-noopener-page-from-no-coop-ro.https.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/resources/noopener-helper.js: Added.
(getExecutorPath):
(const.test_noopener_opening_popup): The logic for the noopener tests.
(async const):
* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/tentative/noopener/coop-noopener-allow-popups.https-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/html/cross-origin-opener-policy/tentative/noopener/coop-noopener-allow-popups.https.html: Added.
* Source/WebCore/loader/CrossOriginOpenerPolicy.cpp:
(WebCore::crossOriginOpenerPolicyToString): Add the "noopener-allow-popups" string.
(WebCore::crossOriginOpenerPolicyValueToEffectivePolicyString): Add the "noopener-allow-popups" string.
(WebCore::matchingCOOP): Implement the related HTML algorithm.
(WebCore::coopValuesRequireBrowsingContextGroupSwitch): Implement the switching logic related to noopener-allow-popups.
(WebCore::obtainCrossOriginOpenerPolicy): Parse the "noopener-allow-popups" value.
* Source/WebCore/loader/CrossOriginOpenerPolicy.h: Add the noopener-allow-popups enum value.
* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in: Add the noopener-allow-popups enum value.

Canonical link: https://commits.webkit.org/284866@main
  • Loading branch information
yoavweiss authored and achristensen07 committed Oct 9, 2024
1 parent 8b10763 commit 7688a5f
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,33 @@ const receiveReport = async function(uuid, type) {
}
}

// Build a set of 'Cross-Origin-Opener-Policy' and
// 'Cross-Origin-Opener-Policy-Report-Only' headers.
const coopHeaders = function (uuid) {
// Use a custom function instead of convertToWPTHeaderPipe(), to avoid
// encoding double quotes as %22, which messes with the reporting endpoint
// registration.
let getHeader = (uuid, coop_value, is_report_only) => {
const header_name =
is_report_only ?
"Cross-Origin-Opener-Policy-Report-Only":
"Cross-Origin-Opener-Policy";
return `|header(${header_name},${coop_value}%3Breport-to="${uuid}")`;
}

return {
coopSameOriginHeader: `|header(Cross-Origin-Opener-Policy,same-origin%3Breport-to="${uuid}")`,
coopSameOriginAllowPopupsHeader: `|header(Cross-Origin-Opener-Policy,same-origin-allow-popups%3Breport-to="${uuid}")`,
coopReportOnlySameOriginHeader: `|header(Cross-Origin-Opener-Policy-Report-Only,same-origin%3Breport-to="${uuid}")`,
coopReportOnlySameOriginAllowPopupsHeader: `|header(Cross-Origin-Opener-Policy-Report-Only,same-origin-allow-popups%3Breport-to="${uuid}")`
coopSameOriginHeader:
getHeader(uuid, "same-origin", is_report_only = false),
coopSameOriginAllowPopupsHeader:
getHeader(uuid, "same-origin-allow-popups", is_report_only = false),
coopRestrictPropertiesHeader:
getHeader(uuid, "restrict-properties", is_report_only = false),
coopReportOnlySameOriginHeader:
getHeader(uuid, "same-origin", is_report_only = true),
coopReportOnlySameOriginAllowPopupsHeader:
getHeader(uuid, "same-origin-allow-popups", is_report_only = true),
coopReportOnlyRestrictPropertiesHeader:
getHeader(uuid, "restrict-properties", is_report_only = true),
coopReportOnlyNoopenerAllowPopupsHeader:
getHeader(uuid, "noopener-allow-popups", is_report_only = true),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

FAIL access-to-coop-page-from-opener, noopener-allow-popups assert_equals: expected (string) "coop" but got (undefined) undefined
FAIL access-to-coop-page-from-opener, noopener-allow-popups + redirect assert_equals: expected (string) "coop" but got (undefined) undefined

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<title>
COOP reports are sent when the openee used COOP-RO+COEP and then its
same-origin opener tries to access it.
</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="/common/dispatcher/dispatcher.js"></script>
<script src="/html/cross-origin-opener-policy/reporting/resources/reporting-common.js"></script>
<script src="/html/cross-origin-opener-policy/reporting/resources/try-access.js"></script>
<script>

const directory = "/html/cross-origin-opener-policy";
const redirect_path = directory + "/resources/redirect.py?";
const same_origin = get_host_info().HTTPS_ORIGIN;

let runTest = (openee_redirect, name) => promise_test(async t => {
const report_token = token();
const openee_token = token();
const opener_token = token(); // The current test window.

const opener_url = location.href;

const reportTo = reportToHeaders(report_token);
const openee_url = same_origin + executor_path + reportTo.header +
reportTo.coopReportOnlyNoopenerAllowPopupsHeader + coep_header +
`&uuid=${openee_token}`;
const openee_redirect_url = same_origin + redirect_path + openee_url
const openee_requested_url = openee_redirect ? openee_redirect_url
: openee_url;


const openee = window.open(openee_requested_url);
t.add_cleanup(() => send(openee_token, "window.close()"))

// 1. Make sure the new document to be loaded.
send(openee_token, `
send("${opener_token}", "Ready");
`);
let reply = await receive(opener_token);
assert_equals(reply, "Ready");

// 2. Try to access the openee. A report is sent, because of COOP-RO+COEP.
tryAccess(openee);

// 3. Check a report is sent to the openee.
let report =
await receiveReport(report_token, "access-to-coop-page-from-opener");
assert_equals(report.type, "coop");
assert_equals(report.url, openee_url.replace(/"/g, '%22'));
assert_equals(report.body.disposition, "reporting");
assert_equals(report.body.effectivePolicy, "noopener-allow-popups");
assert_equals(report.body.property, "blur");
assert_source_location_missing(report);
assert_equals(report.body.openerURL, opener_url);
assert_equals(report.body.openeeURL, undefined);
assert_equals(report.body.otherDocumentURL, undefined);
assert_equals(report.body.referrer, opener_url);
assert_equals(report.body.initialPopupURL, undefined);
}, name);

runTest(false, "access-to-coop-page-from-opener, noopener-allow-popups");
runTest(true , "access-to-coop-page-from-opener, noopener-allow-popups + redirect");

</script>

Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const executor_path = '/common/dispatcher/executor.html?pipe=';
const coop_header = policy => {
return `|header(Cross-Origin-Opener-Policy,${policy})`;
};

function getExecutorPath(uuid, origin, coop_header) {
return origin.origin + executor_path + coop_header + `&uuid=${uuid}`;
}

const test_noopener_opening_popup =
(opener_coop, openee_coop, origin, opener_expectation) => {
promise_test(async t => {
// Set up dispatcher communications.
const popup_token = token();
const reply_token = token();
const popup_reply_token = token();
const popup_openee_token = token();

const popup_url = getExecutorPath(
popup_token, SAME_ORIGIN, coop_header(opener_coop));

// We open a popup and then ping it, it will respond after loading.
const popup = window.open(popup_url);
t.add_cleanup(() => send(popup_token, 'window.close()'));
send(popup_token, `send('${reply_token}', 'Popup loaded');`);
assert_equals(await receive(reply_token), 'Popup loaded');

if (opener_coop == 'noopener-allow-popups') {
// Assert that we can't script the popup.
assert_true(popup.closed, 'Opener popup.closed');
}

// Ensure that the popup has no access to its opener.
send(popup_token, `
let openerDOMAccessAllowed = false;
try {
openerDOMAccessAllowed = !!self.opener.document.URL;
} catch(ex) {
}
const payload = {
opener: !!self.opener,
openerDOMAccess: openerDOMAccessAllowed
};
send('${reply_token}', JSON.stringify(payload));
`);
let payload = JSON.parse(await receive(reply_token));
if (opener_coop == 'noopener-allow-popups') {
assert_false(payload.opener, 'popup opener');
assert_false(payload.openerDOMAccess, 'popup DOM access');
}

// Open another popup from inside the popup, and wait for it to
// load.
const popup_openee_url = getExecutorPath(
popup_openee_token, origin, coop_header(openee_coop));
send(popup_token, `
window.openee = open("${popup_openee_url}");
await receive('${popup_reply_token}');
const payload = {
openee: !!window.openee,
closed: window.openee.closed
};
send('${reply_token}', JSON.stringify(payload));
`);
t.add_cleanup(() => send(popup_token, 'window.openee && window.openee.close()'));
// Notify the popup that its openee was loaded.
send(popup_openee_token, `send('${popup_reply_token}', 'popup openee opened');`);

// Assert that the popup has access to its openee.
payload = JSON.parse(await receive(reply_token));
assert_true(payload.openee, 'popup openee');

// Assert that the openee has access to its popup opener.
send(popup_openee_token, `
let openerDOMAccessAllowed = false;
try {
openerDOMAccessAllowed = !!self.opener.document.URL;
} catch(ex) {
}
const payload = {
opener: !!self.opener,
openerDOMAccess: openerDOMAccessAllowed
};
send('${reply_token}', JSON.stringify(payload));
`);
payload = JSON.parse(await receive(reply_token));
if (opener_expectation) {
assert_true(payload.opener, 'Opener is not null');
assert_true(payload.openerDOMAccess, 'No DOM access');
} else {
assert_false(payload.opener, 'Opener is null');
assert_false(payload.openerDOMAccess, 'No DOM access');
}
},
'noopener-allow-popups ensures that the opener cannot script the openee,' +
' but further popups with no COOP can access their opener: ' +
opener_coop + '/' + openee_coop + ':' + origin == SAME_ORIGIN);
};

const test_noopener_navigating_away = (popup_coop) => {
promise_test(
async t => {
// Set up dispatcher communications.
const popup_token = token();
const reply_token = token();
const popup_reply_token = token();
const popup_second_token = token();

const popup_url =
getExecutorPath(popup_token, SAME_ORIGIN, coop_header(popup_coop));

// We open a popup and then ping it, it will respond after loading.
const popup = window.open(popup_url);
send(popup_token, `send('${reply_token}', 'Popup loaded');`);
assert_equals(await receive(reply_token), 'Popup loaded');
t.add_cleanup(() => send(popup_token, 'window.close()'));

// Assert that we can script the popup.
assert_not_equals(popup.window, null);
assert_false(popup.closed, 'popup closed');

// Ensure that the popup has no access to its opener.
send(popup_token, `
let openerDOMAccessAllowed = false;
try {
openerDOMAccessAllowed = !!self.opener.document.URL;
} catch(ex) {
}
const payload = {
opener: !!self.opener,
openerDOMAccess: openerDOMAccessAllowed
};
send('${reply_token}', JSON.stringify(payload));
`);
let payload = JSON.parse(await receive(reply_token));
assert_true(payload.opener, 'popup opener');
assert_true(payload.openerDOMAccess, 'popup DOM access');

// Navigate the popup
const popup_second_url = getExecutorPath(
popup_second_token, SAME_ORIGIN,
coop_header('noopener-allow-popups'));

send(popup_token, `
window.location = '${popup_second_url}';
`);

// Notify the popup that its openee was loaded.
send(
popup_second_token,
`send('${reply_token}', 'popup navigated away');`);
assert_equals(await receive(reply_token), 'popup navigated away');

//assert_equals(popup.window, null);
assert_true(popup.closed, 'popup.closed');
},
'noopener-allow-popups ensures that the opener cannot script the openee,' +
' even after a navigation: ' + popup_coop);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.

PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
1
PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
2
PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
3
PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
4
PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
5
PASS
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
6
PASS noopener-allow-popups ensures that the opener cannot script the openee, even after a navigation: unsafe-none

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!doctype html>
<title>
Cross-Origin-Opener-Policy: noopener-allow-popups means that the opener
has no access to the openee.
</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="/common/utils.js"></script>
<script src="../../resources/common.js"></script>
<script src="../../resources/noopener-helper.js"></script>
<script>


test_noopener_opening_popup("noopener-allow-popups",
"unsafe-none",
SAME_ORIGIN,
/*opener_expectation=*/true);
test_noopener_opening_popup("noopener-allow-popups",
"noopener-allow-popups",
SAME_ORIGIN,
/*opener_expectation=*/false);
test_noopener_opening_popup("noopener-allow-popups",
"same-origin",
SAME_ORIGIN,
/*opener_expectation=*/false);
test_noopener_opening_popup("noopener-allow-popups",
"same-origin-allow-popups",
SAME_ORIGIN,
/*opener_expectation=*/false);
test_noopener_opening_popup("noopener-allow-popups",
"same-origin-allow-popups",
CROSS_ORIGIN,
/*opener_expectation=*/false);
test_noopener_opening_popup("same-origin-allow-popups",
"noopener-allow-popups",
SAME_ORIGIN,
/*opener_expectation=*/false);
test_noopener_opening_popup("same-origin",
"noopener-allow-popups",
SAME_ORIGIN,
/*opener_expectation=*/false);
test_noopener_navigating_away("unsafe-none");
</script>
Loading

0 comments on commit 7688a5f

Please sign in to comment.