Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

  • This WPT test may be referenced by the following Test IDs:
// META: script=/common/get-host-info.sub.js
// META: script=/common/utils.js
// META: script=/common/dispatcher/dispatcher.js
//
// This is a regression test for crbug.com/583445. It checks an obscure bug in
// Chromium's handling of `document.open()` whereby the URL change would affect
// the document's origin after a javascript navigation.
//
// See also dcheng@'s comments on the original code review in which he
// introduced the precursor to this test:
function nextMessage() {
return new Promise((resolve) => {
window.addEventListener("message", (e) => { resolve(e.data); }, {
once: true
});
});
}
promise_test(async (t) => {
// Embed a cross-origin frame A and set up remote code execution.
const iframeA = document.body.appendChild(document.createElement("iframe"));
t.add_cleanup(() => { iframeA.remove(); });
const uuidA = token();
iframeA.src = remoteExecutorUrl(uuidA, { host: get_host_info().REMOTE_HOST });
const ctxA = new RemoteContext(uuidA);
// Frame A embeds a cross-origin frame B, which is same-origin with the
// top-level frame. Frame B is the center of this test: it is where we will
// verify that a bug does not grant it UXSS in frame A.
//
// Though we could reach into `iframeA.frames[0]` to get a proxy to frame B
// and use `setTimeout()` like below to execute code inside it, we set up
// remote code execution using `dispatcher.js` for better ergonomics.
const uuidB = token();
await ctxA.execute_script((url) => {
const iframeB = document.createElement("iframe");
iframeB.src = url;
document.body.appendChild(iframeB);
}, [remoteExecutorUrl(uuidB).href]);
// Start listening for a message, which will come as a result of executing
// the code below in frame B.
const message = nextMessage();
const ctxB = new RemoteContext(uuidB);
await ctxB.execute_script(() => {
// Frame B embeds an `about:blank` frame C.
const iframeC = document.body.appendChild(document.createElement("iframe"));
// We wish to execute code inside frame C, but it is important to this test
// that its URL remain `about:blank`, so we cannot use `dispatcher.js`.
// Instead we rely on `setTimeout()`.
//
// We use `setTimeout(string, ...)` instead of `setTimeout(function, ...)`
// as the given script executes against the target window's global object
// and does not capture any local variables.
//
// In order to have nice syntax highlighting and avoid quote-escaping hell,
// we use a trick employed by `dispatcher.js`. We rely on the fact that
// functions in JS have a stringifier that returns their source code. Thus
// `"(" + func + ")()"` is a string that executes `func()` when evaluated.
iframeC.contentWindow.setTimeout("(" + (() => {
// This executes in frame C.
// Frame C calls `document.open()` on its parent, which results in B's
// URL being set to `about:blank` (C's URL).
//
// However, just before `document.open()` is called, B schedules a
// self-navigation to a `javascript:` URL. This will occur after
// `document.open()`, so the document will navigate from `about:blank` to
// the new URL.
//
// This should not result in B's origin changing, so B should remain
// same-origin with the top-level frame.
//
// Due to crbug.com/583445, this used to behave wrongly in Chromium. The
// navigation code incorrectly assumed that B's origin should be inherited
// from its parent A because B's URL was `about:blank`.
//
// It is important to schedule this from within the child, as this
// guarantees that `document.open()` will be called before the navigation.
// A previous version of this test scheduled this from within frame B
// right after scheduling the call to `document.open()`, but that ran the
// risk of races depending on which timeout fired first.
parent.window.setTimeout("(" + (() => {
// This executes in frame B.
location = "javascript:(" + (() => {
/* This also executes in frame B.
*
* Note that because this whole function gets stuffed in a JS URL,
* single-line comments do not work, as they affect the following
* lines. */
let error;
try {
/* This will fail with a `SecurityError` if frame B is no longer
* same-origin with the top-level frame. */
top.window.testSameOrigin = true;
} catch (e) {
error = e;
}
top.postMessage({
error: error?.toString(),
}, "*");
}) + ")()";
}) + ")()", 0);
// This executes in frame C.
parent.document.open();
}) + ")()", 0);
});
// Await the message from frame B after its navigation.
const { error } = await message;
assert_equals(error, undefined, "error accessing top frame from frame B");
assert_true(window.testSameOrigin, "top frame testSameOrigin is mutated");
}, "Regression test for crbug.com/583445");