skip to content

A community site renders user-submitted links with `target="_blank"`. Explain reverse tabnabbing through `window.opener`, and what you would do about it given how browsers behave today.

level: seniorimportance: should knowfreq 45%

answer

  1. the new tab gets a handle back
  2. reading is blocked, navigating is not
  3. the tab you left is swapped
  4. rel=noopener, noreferrer
  5. window.open needs it explicitly

basics

~20 s

A page opened with target="_blank" gets a window.opener handle and, even cross-origin, may assign window.opener.location to redirect the tab the user came from to a phishing copy. Add rel="noopener" to user-submitted links, and pass 'noopener' to window.open, which modern anchor defaults do not cover.

solid answer

~50 s

When a link opens a new browsing context, that context gets `window.opener` pointing back at yours. The same-origin policy blocks reading the opener's DOM, but *navigating* it is cross-origin-allowed: the destination can run `window.opener.location = 'https://phish.example/login'` and quietly replace the tab the user left behind. The user finishes reading, switches back, and finds what looks like your site asking them to log in again — the address bar shows a URL they never typed and are not looking at. The fix is `rel="noopener"` (or `rel="noreferrer"`, which implies it and also drops the Referer header), which makes `window.opener` null in the new context. Since roughly Chrome 88, Firefox 79 and Safari 12.1, anchors with `target="_blank"` behave as if `noopener` were set, but I still emit it explicitly for older clients and non-anchor cases, and `window.open()` has no such default.

code

javascript · 17 lines
javascript
// Rendering a link the user submitted
function renderExternalLink(href, text) {
  const url = new URL(href, location.href);
  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
    throw new Error('unsupported scheme: ' + url.protocol); // blocks javascript: and data:
  }
  const a = document.createElement('a');
  a.href = url.href;
  a.target = '_blank';
  a.rel = 'noopener noreferrer'; // explicit, not relying on the modern anchor default
  a.textContent = text;
  return a;
}

// Script-opened windows get no such default:
const win = window.open('https://untrusted.example', '_blank', 'noopener');
console.log(win); // null

go deeper

for a junior

Know that target="_blank" hands the new page a window.opener reference, and that rel="noopener" is what you add to links you do not control.

for a middle

Explain the asymmetry that makes it work: cross-origin script cannot read the opener, but assigning window.opener.location is allowed, so the destination can replace the original tab.

for a senior

Describe the whole hardening pass on user-submitted links — scheme validation, noopener plus noreferrer, awareness of which browser defaults already cover you — and be honest about what the modern anchor default does and does not reach.

for a principal

Decide the policy: which link surfaces are treated as untrusted by construction, how it is enforced (a shared link component or sanitizer rather than reviewer discipline), and where legitimate opener-dependent flows such as OAuth popups are allowed to opt back in.

## What window.opener is When a document opens a new browsing context — through `<a target="_blank">`, `<form target="_blank">` or `window.open()` — the new context's `window.opener` references the opener's `WindowProxy`. That reference survives navigation in the new tab and is a normal part of the platform: OAuth popups, print helpers and "open in a new tab and post the result back" flows depend on it. The same-origin policy applies to it in the usual asymmetric way. Cross-origin, the opened page cannot read `window.opener.document`, cookies, or any script state. But a handful of `Window` members are deliberately cross-origin accessible, and one of them is **writing** `location`. Assigning `window.opener.location = url` is a navigation request, and navigating a window you have a handle to has always been permitted. ## The attack Reverse tabnabbing exploits exactly that asymmetry plus a human factor: users do not re-verify a tab they already had open. 1. An attacker posts a link on your forum. A user clicks it; it opens in a new tab with `target="_blank"`. 2. The attacker's page runs `window.opener.location = 'https://forum-example.attacker.test/session-expired'` — a pixel copy of your login page. 3. The user reads the linked page, closes or switches away from it, and returns to what they believe is the tab they left. 4. They see "your session expired, please sign in" and type real credentials into the attacker's page. Nothing about the original tab visibly changed except its URL, which nobody re-reads. The attack needs no XSS on your site and no interaction beyond a click on a link you rendered. A related, milder issue is that without `noreferrer` the destination receives a `Referer` header disclosing the page the user came from, which can leak a private URL. ## The fixes `rel="noopener"` on the anchor makes the new context's `window.opener` **null**, so there is nothing to navigate: ```html <a href="https://untrusted.example/thread" target="_blank" rel="noopener noreferrer"> User-submitted link </a> ``` `rel="noreferrer"` is a superset: it also suppresses the `Referer` header, and it implies `noopener`. For links to third-party content that a user submitted, both together is the sensible default; for links to a partner who needs the referrer for attribution, `noopener` alone. For script-opened windows, the token goes in the *features* string, which is the part people forget: ```js const win = window.open(url, '_blank', 'noopener'); // win is null — with noopener there is no handle to give back ``` That `null` return is the tell that it worked, and also the reason the pattern of `const w = window.open(...); w.opener = null;` is inferior: it runs after the context exists, does not apply if the new page navigates first, and depends on your script running at all. ## What changed in browsers Browsers eventually made the safe behaviour the default for anchors. As of the current major browsers — the change landed in Chrome 88, Firefox 79 and Safari 12.1 — a link with `target="_blank"` implies `noopener` unless you explicitly opt back in with `rel="opener"`. `window.open()` was deliberately **not** changed, because its return value is the whole point of calling it and defaulting to `null` would have broken popup flows across the web. So the current posture is: anchors are safe by default in modern browsers, but you still write `rel="noopener noreferrer"` on untrusted links because it is free, it documents intent, it protects users on old clients, and it survives markup being copied into a context where the default does not apply. ## The rest of the hardening for user-submitted links The opener problem is one of a family, and a senior answer covers the neighbours: - **Validate the scheme.** Allow `http:` and `https:` (and possibly `mailto:`); reject `javascript:` and `data:` hrefs outright, since those execute in *your* origin. Parse with `new URL(href, location.href)` and check `protocol`. - **Do not surface a link's real destination only in the status bar.** Displaying the resolved hostname next to the link text blunts lookalike domains. - **Keep the tradeoff in mind for `noopener`.** Dropping the opener relationship allows the browser to open the new page in a separate process, which is a small memory cost and a performance benefit; it also breaks any legitimate flow that expects to post back to the opener, which is why the token belongs on untrusted links rather than blanket-applied to your own OAuth popups. The short form for an interview: `window.opener` lets an opened page navigate the tab that opened it, that is enough for a credible phishing swap, `rel="noopener"` removes the reference, anchors have defaulted to it for years now, and `window.open` still needs it passed by hand.

  • What is the difference between `rel="noopener"` and `rel="noreferrer"`?
    `noopener` nulls `window.opener` in the opened context, removing the ability to navigate the tab that opened it. `noreferrer` does that too — it implies `noopener` — and additionally suppresses the `Referer` header, so the destination cannot see which page the user came from. Use `noreferrer` for untrusted links, `noopener` alone when a partner legitimately needs referrer attribution.
  • If modern browsers already imply `noopener` for `target="_blank"` anchors, why keep writing it?
    Because the default covers only anchors, only on browsers that shipped the change, and only when the markup stays an anchor. Script-opened windows, older embedded webviews, and markup copied into other contexts all miss it. The attribute costs nothing, states intent for reviewers, and removes the need to reason about which client the user is on.
  • How do you open a window without an opener but still keep a handle to it?
    You cannot: `window.open(url, '_blank', 'noopener')` returns `null` precisely because handing back a handle would recreate the relationship in the other direction. If your flow needs two-way communication — an OAuth popup, for example — keep the opener and rely on strict `targetOrigin` and `event.origin` checks instead of severing the link.

saying these in an interview costs you the question

  • Claiming the same-origin policy blocks navigating the opener
  • Thinking the opened page can read the opener's DOM or cookies
  • Assuming window.open defaults to noopener like anchors do
  • Setting w.opener = null after open and calling it equivalent
  • Believing noopener also sanitizes javascript: hrefs

context