tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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");