Step-by-step diagram of reverse tabnabbing: target=_blank link, window.opener reference, the silent redirect of the parent tab, and the three fixes — rel=noopener, rel=noreferrer, and Cross-Origin-Opener-Policy Web Security & Browser Isolation • Sensei Learning System

Reverse Tabnabbing: How window.opener Lets a New Tab Redirect the Old One

A new tab you just opened quietly holds a handle on the tab that opened it — and that handle is writable, not readable. That asymmetry is the whole attack.

💡 Core Concept

When you open a link with target="_blank", the browser hands the new page a window.opener reference back to the page that launched it. Same-origin policy still blocks the new page from reading your DOM — but it does not block it from steering you.

Cross-origin scripts in the new tab cannot touch your cookies, tokens, or localStorage. What they can do is write to window.opener.location — silently navigating your original tab to a look-alike phishing page while your eyes stay on the new one. That redirect-the-parent trick is reverse tabnabbing.

window.opener ≠ null  ⇒  the new tab can navigate the old one

🧱 Key Components

The opener handle

target="_blank" (and window.open()) may hand the new page a live reference back to its parent. That reference is the attack surface.

Write, not read

Same-origin policy blocks every read of your DOM, cookies, tokens and storage. Writing opener.location is allowed — control without data access.

Three ways to cut it

rel="noopener" nulls the handle; rel="noreferrer" also hides the Referer header and implies noopener; COOP severs opener links at document level.

Modern browsers

Chrome 88+, Firefox 79+, Safari 12.1+ default plain target="_blank" anchors to implicit noopener — the live risk concentrates in window.open(), older browsers, and webviews.

⚙️ How It Works

  1. The handle is created. Tab A follows a link with target="_blank". Tab B opens, and the browser wires window.opener in B to point at A.
  2. Reads stay blocked. B is on a different origin, so every attempt to read A’s DOM, cookies or tokens hits the same-origin policy. No data leaks.
  3. The write slips through. B’s script sets window.opener.location = "https://evil.example/login". Writing a location is navigation, not a data read — and it’s allowed.
  4. Tab A jumps while you watch B. Your original tab silently loads a convincing fake-login clone. You keep reading tab B, so the swap goes unnoticed — until you switch back and type your password.
  5. The fixes sever the handle. rel="noopener" makes window.opener null; rel="noreferrer" adds Referer suppression; COOP cuts opener relationships document-wide.

📑 Verify it yourself — the falsification test

Host two pages on different origins. From the opened page, run window.opener.location = "https://evil.example" and watch the parent tab navigate — that proves the control.

Then try window.opener.document.cookie: the same-origin error is the point. You have control without read access. Re-run with rel="noopener" and confirm window.opener is now null.

⚖️ Calibrating the Scary Claim

The usual warning — “a new page gets partial, potentially malicious access to your original page” — is true only in a narrow sense:

“Access” here means navigation control plus a few non-readable window methods. It is not data access: a cross-origin opener cannot read your cookies, auth tokens, localStorage, or DOM content.

And it is not a universal live hole — modern desktop browsers auto-apply noopener to plain target="_blank" anchors. The risk concentrates in window.open() calls, older browsers, and embedded webviews (apps, Electron shells, in-house browsers).

🌐️ Where You’ll Meet It

🔒

Credential-phishing redirects. The classic payload: swap your original tab onto a clone of the login page you were about to use. The user never notices the swap — they just land on a fake page that looks exactly like home.

⚙️

window.open() is the real hole. The implicit-noopener default applies to plain anchor links; a hand-rolled window.open(url, "_blank") in your own site’s JS can still create a live opener on browsers that don’t cover that path — review open-handling code, not just anchor HTML.

📱

Embedded webviews. In-app browsers, Electron, and kiosk webviews are common homes for older engine versions where the implicit-noopener default never landed — the attack surface lives inside apps, not on the open web.

🛡️

Document-level defense. Cross-Origin-Opener-Policy (with COEP for isolation) lets a site sever opener relationships for its own documents — the header-level cousin of the per-anchor rel fixes.

✅️ Checkpoint — answer before you move on

  1. Why can a cross-origin page in tab B navigate your tab A even though it can’t read it? What does writing location do, and why isn’t it classified as a data read under same-origin policy?
  2. What exactly does rel="noreferrer" give you that rel="noopener" alone does not? One of them is a navigation fix; the other also touches a network header.
  3. Walk the falsification test: what observation proves “control without read access,” and what single attribute change makes window.opener null? The cookie read fails for a reason; the anchor attribute that nulls the handle is two letters short of a word.