🅭

Mitigate “tabnabbing” without breaking window.open() features

The security issues and performance hit of websites opening a new window are fairly well understood. However, applying the rel=noopener mitigation has its own drawbacks and isn’t backward-compatible.

In this article, I’ll focus exclusively on the problems introduced along with support for rel=noopener in Google Chrome version 49. The above links were your queue to read-up on it if you’re unfamiliar with the issue.

Understandably, these pop-ups were designed with the ability to change the primary document in their “opener” windows. A supplementary help navigation window could link users to the right document. This feature was largely forgotten by the web development community until security cautious developers realized the inherent security issue in letting third-parties arbitrarily redirect the opener page just because they’d been opened in a new window.

On websites designed for PCs, you occasionally encounter a situation were websites wants to create a small complimentary window. Ignoring the pop-up advertisement epidemic of the late 1990s and early 2000s, there are still some legitimate use-cases for these windows, such as chat tools, supplementary documentation, and video pop-outs. These uses translate into creating new tabs in mobile web browsers as the platforms don’t support traditional computer-windowing concepts.

However, Google Chrome version 49 up 77 stopped supporting window.open()-features when combined with the standard noopener feature. Chrome 77 is the current stable version, at the time of publication. For example, you could either set the size or position of a newly opened window or unset the window.opener. Whenever the latter feature is used, all other window features are ignored.

This means that the following two examples will create a window with the exact same size and screen position in Google Chrome. The supplied size and position constraints of the second example are ignored.

window.open(
  "https://example.com/",
  null,
  "noopener=1"
);
window.open(
  "https://example.com/",
  null,
  "noopener=1,height=500,width=500,top=100,left=100"
);

The second example works just fine in recent versions of Firefox and Safari.

You can create achieve the same effect as using the noopener and get Chrome-compatibility and better backward-compatibility with older web browsers at the same time. I’ll walk you through the process.

First, start by creating an empty window (using either an empty string or about:blank) with the desired dimensions and position. The following examples create a new 500x500 pixel window at 100 px from the top left corner of the active screen. The window is stored in the popup variable.

var popup = window.open(
  "about:blank",
  null,
  "height=500,width=500,top=100,left=100"
);

You’ve now opened an untitled empty window with the right dimension and position. You can then proceed to give the user some feedback by setting the temporary window’s title. If you don’t set a title, the user may be staring at a window “Untitled” for some time. You can close the door on tabnabbing by explicitly unsetting the popup’s opener. Setting it to null gets the job done.

Lastly, you can redirect the empty page to the destination page. The following example redirects the popup we created above to https://example.com/.

popup.document.title = "Loading…";
popup.opener = null;
popup.location.replace(
    "https://example.com/"
);

The HTML Specification, as of the 2019-09-28 edition, leaves every feature other than noopener undefined. I can’t really call Google Chrome’s behavior a bug, even though it deviates from all the other web browsers. The issue also affect other Chromium-based web browsers such as Brave, Opera, and Vivaldi.

I’ve tried to report this issue to the Chromium project. However, the bug submission form returns an HTTP 400 error message on submission. Let the record show that I tried.