crbug-583445-regression.window.js (5158B)
1 // META: script=/common/get-host-info.sub.js 2 // META: script=/common/utils.js 3 // META: script=/common/dispatcher/dispatcher.js 4 // 5 // This is a regression test for crbug.com/583445. It checks an obscure bug in 6 // Chromium's handling of `document.open()` whereby the URL change would affect 7 // the document's origin after a javascript navigation. 8 // 9 // See also dcheng@'s comments on the original code review in which he 10 // introduced the precursor to this test: 11 // https://codereview.chromium.org/1675473002. 12 13 function nextMessage() { 14 return new Promise((resolve) => { 15 window.addEventListener("message", (e) => { resolve(e.data); }, { 16 once: true 17 }); 18 }); 19 } 20 21 promise_test(async (t) => { 22 // Embed a cross-origin frame A and set up remote code execution. 23 const iframeA = document.body.appendChild(document.createElement("iframe")); 24 t.add_cleanup(() => { iframeA.remove(); }); 25 26 const uuidA = token(); 27 iframeA.src = remoteExecutorUrl(uuidA, { host: get_host_info().REMOTE_HOST }); 28 const ctxA = new RemoteContext(uuidA); 29 30 // Frame A embeds a cross-origin frame B, which is same-origin with the 31 // top-level frame. Frame B is the center of this test: it is where we will 32 // verify that a bug does not grant it UXSS in frame A. 33 // 34 // Though we could reach into `iframeA.frames[0]` to get a proxy to frame B 35 // and use `setTimeout()` like below to execute code inside it, we set up 36 // remote code execution using `dispatcher.js` for better ergonomics. 37 const uuidB = token(); 38 await ctxA.execute_script((url) => { 39 const iframeB = document.createElement("iframe"); 40 iframeB.src = url; 41 document.body.appendChild(iframeB); 42 }, [remoteExecutorUrl(uuidB).href]); 43 44 // Start listening for a message, which will come as a result of executing 45 // the code below in frame B. 46 const message = nextMessage(); 47 48 const ctxB = new RemoteContext(uuidB); 49 await ctxB.execute_script(() => { 50 // Frame B embeds an `about:blank` frame C. 51 const iframeC = document.body.appendChild(document.createElement("iframe")); 52 53 // We wish to execute code inside frame C, but it is important to this test 54 // that its URL remain `about:blank`, so we cannot use `dispatcher.js`. 55 // Instead we rely on `setTimeout()`. 56 // 57 // We use `setTimeout(string, ...)` instead of `setTimeout(function, ...)` 58 // as the given script executes against the target window's global object 59 // and does not capture any local variables. 60 // 61 // In order to have nice syntax highlighting and avoid quote-escaping hell, 62 // we use a trick employed by `dispatcher.js`. We rely on the fact that 63 // functions in JS have a stringifier that returns their source code. Thus 64 // `"(" + func + ")()"` is a string that executes `func()` when evaluated. 65 iframeC.contentWindow.setTimeout("(" + (() => { 66 // This executes in frame C. 67 68 // Frame C calls `document.open()` on its parent, which results in B's 69 // URL being set to `about:blank` (C's URL). 70 // 71 // However, just before `document.open()` is called, B schedules a 72 // self-navigation to a `javascript:` URL. This will occur after 73 // `document.open()`, so the document will navigate from `about:blank` to 74 // the new URL. 75 // 76 // This should not result in B's origin changing, so B should remain 77 // same-origin with the top-level frame. 78 // 79 // Due to crbug.com/583445, this used to behave wrongly in Chromium. The 80 // navigation code incorrectly assumed that B's origin should be inherited 81 // from its parent A because B's URL was `about:blank`. 82 // 83 // It is important to schedule this from within the child, as this 84 // guarantees that `document.open()` will be called before the navigation. 85 // A previous version of this test scheduled this from within frame B 86 // right after scheduling the call to `document.open()`, but that ran the 87 // risk of races depending on which timeout fired first. 88 parent.window.setTimeout("(" + (() => { 89 // This executes in frame B. 90 91 location = "javascript:(" + (() => { 92 /* This also executes in frame B. 93 * 94 * Note that because this whole function gets stuffed in a JS URL, 95 * single-line comments do not work, as they affect the following 96 * lines. */ 97 98 let error; 99 try { 100 /* This will fail with a `SecurityError` if frame B is no longer 101 * same-origin with the top-level frame. */ 102 top.window.testSameOrigin = true; 103 } catch (e) { 104 error = e; 105 } 106 107 top.postMessage({ 108 error: error?.toString(), 109 }, "*"); 110 111 }) + ")()"; 112 113 }) + ")()", 0); 114 115 // This executes in frame C. 116 parent.document.open(); 117 118 }) + ")()", 0); 119 }); 120 121 // Await the message from frame B after its navigation. 122 const { error } = await message; 123 assert_equals(error, undefined, "error accessing top frame from frame B"); 124 assert_true(window.testSameOrigin, "top frame testSameOrigin is mutated"); 125 126 }, "Regression test for crbug.com/583445");