December 7, 2023

Browsing Context, WindowProxy, Window

Every Frontend developer knows what a Window object is. Everything seems to be clear with the object itself. But upon closer examination, it turns out that the browser never gives this important global object directly. In this article, I propose to understand the HTML specification and how exactly the browser behaves in terms of the global context.

It's worth starting with the fact that there may be several global Window objects in a document (for example, an iframe on a page). Moreover, there may also be several documents themselves (for example, open browser tabs). There may be some connectivity between documents and their Window objects via frame.contentWindow, self.opener, window.open(), etc. According to the specification, every browser document, be it a tab, iframe or anything else, is so-called navigable. In turn, each navigable in the HTML specification is called a browsing context. The browsing context itself has associated WindowProxy and Window objects. When we switch the context, the Window object changes to the appropriate one, but the WindowProxy is always the same.

The fact is that WindowProxy is like a universal wrapper proxy for a variety of Windows. It transparently gives away all the available properties of the Window, however, the Window itself may change from time to time. Also, unlike Window, which is an ordinary object, WindowProxy is an exotic object. Let me remind you that all classic objects in JavaScript that we can freely create are considered ordinary. Those objects whose behavior may have some hidden logic are considered exotic, such objects include, for example, Array, String, Arguments, etc.

WindowProxy, being exotic, has a slot [[Window]], as well as a number of hidden methods: getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions, GetOwnProperty, which are designed to ensure the operation of the object as a proxy.

Why do we need such difficulties, isn't it easier to just give the Window as it is? The answer is simple, the main reason is the security policy. We know that the same-origin policy prohibits access to the code of one origin from another. I.e. it is not allowed from the domain a.com get access to the code b.com . However, for example, if b.com was placed inside a.com through the iframe, there are some policy exceptions and access to the code can all be obtained, which potentially creates security holes.

Let's imagine the following code

What does this code do?

// we hang the onload callback on the iframe, which will work
// when loading/reloading the frame
frame.onload = function onFrameLoad() {
  // after loading the frame, we will save the link to a `contentWindow`
  cWindow = frame.contentWindow;
  // and also, let's create a global variable `a` in the context of Window
  cWindow.a = "a";
  
  // Displaying the result on the screen.
  //
  // The function below compares the link to the saved contentWindow
  // with real contentWindow:
  //
  // cWindow === frame.contentWindow
  //
  // and also outputs the value of the global variable `a` from the Window
  //
  // const printResult = (id) => {
  //   const isEqual = cWindow === frame.contentWindow;
  //   const value = cWindow.a;
  //
  //   document.getElementById(id).innerHTML = `contentWindow is equal: ${isEqual};\nvalue: ${value}`;
  // }
  printResult("result-1");
  
  // next, we will hang a new callback on the onload of the same iframe
  frame.onload = function () {
    // and once again we will output the result after reloading the frame
    printResult("result-2");
  }
}

We see that after the frame is loaded for the first time, the сWindow link is identical to frame.contentWindow, and the global variable window.a = 'a', as expected.

After loading the frame for the second time, we see that cWindow is still identical to frame.contentWindow, but window.a does not exist in this context. From the perspective of security policy, this is correct, the iframe should not be able to mutate the parent global object. This and other security issues in child contexts have been the subject of headaches and detailed discussion by Bobby Holley, Boris Zbarsky, Ian Hickson, Adam Barth, Domenic Denicola, and Anne van Kesteren over the years, which completed in the "Define security around Window, WindowProxy, and Location properly" pool request.

But how was this technically possible? After all, the link to the context has not changed? How can a variable exist and not exist at the same time in the same context?

It's all the cause of WindowProxy. As I said before, when working with the Window object, we, in fact, always refer to WindowProxy, which provides isolation of the Window inside the browsing context. At the same time, the link to the WindowProxy itself remains permanent. That is why cWindow === frame.contentWindow always returns true, both links are links to WindowProxy. However, after reloading the frame, we created a second Window context, which will exist inside the WindowProxyManager. By accessing the global variable, WindowProxy determines which context the call is from and puts the corresponding Window in the [[Window]] slot.

My telegram channels:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

Русская версия: https://blog.frontend-almanac.ru/S-r3ZrVCZ0Z