COOP & Tabnabbing / proof of concept
Does the opener handle survive?
A retained window.opener reference lets one origin silently navigate
another's tab. Cross-Origin-Opener-Policy is supposed to sever it.
These three tests establish, in a real browser, whether it actually does — and
which of the vectors browsers left open still apply.
Authorized testing only. Opening a third-party site through this page is a request from your browser, attributable to you. Everything runs locally — no URL, result or log leaves this tab.
Target
step 1http:// or https:// only · embedded credentials rejected
seconds
handle.location. If the navigation lands, the proof page is
same-origin with this tool — so the new URL can be read back, making
the verdict a measurement rather than an inference.
Shareable link
built livePre-fills the target for whoever opens it — and nothing more. The recipient still confirms authorization and presses Launch. A popup needs a user gesture regardless, so an auto-firing link would be blocked as well as unsafe.
Test environment
capture with evidenceWhy this mode exists. Since Chrome 88, Firefox 79 and
Safari 12.1 a plain <a target="_blank"> gets implicit
noopener, so clients routinely dismiss tabnabbing as fixed. It is not.
window.open(), <form target="_blank"> and
rel="opener" still hand over a live handle.
Each row opens a same-origin probe on this site — never your
target — so the probe can read its own window.opener and report back
truthfully. This measures the browser you are holding, which is the baseline that
makes a finding hard to argue with.
Opener-handle vectors
popups must be allowed| Vector | Mechanism | Expected | Observed | Run |
|---|
This is the finding most applications actually have. Modes 1 and 2 test whether a site protects itself. This tests whether the client's own outbound links hand a handle to third parties — reverse tabnabbing per OWASP WSTG-CLNT-14.
Probe URL
Point a link, form or window.open() call on the
target application at this URL, then activate it. The probe reports exactly
what it received.
Drop-in snippets
3 vectorsVector A — anchor
Vector B — form
Vector C — window.open
Finding write-up
Report-ready language. The evidence pack in mode 1 pulls your actual observations into this structure.
Impact — what an attacker gains
1 · Silent phishing by tab replacement. The user clicks a link, skims the new tab, and returns to the original later. In between, the opened page rewrote that original tab into a credential-harvesting clone. No navigation was witnessed, and the tab was one the user opened themselves — so it carries their trust. This defeats "check the URL before typing your password", because the URL changed while they were looking elsewhere.
2 · Cross-window reach. A retained handle permits navigation across origins, and full DOM access for same-origin pairs. Attacker-controlled content reachable from the application inherits a foothold it should not have.
3 · No cross-origin isolation. Without COOP the document never
becomes crossOriginIsolated, so SharedArrayBuffer and
high-resolution timers stay unavailable — a functional cost alongside the security one.
4 · XS-Leaks surface. A retained handle enables frame-counting and navigation-timing side channels used to infer authenticated state. See the XS-Leaks Wiki.
Reproduction steps
Inbound — does the target protect itself?
1 · Open this tool. 2 · Enter the target URL. 3 · Confirm authorization and press Launch. 4 · The target opens in a new tab and loads normally. 5 · After the delay, watch that tab navigate to this tool's proof page with no interaction inside it. 6 · The verdict panel confirms the tool read the new location back.
Outbound — do the target's links leak?
1 · Copy the probe URL from mode 3. 2 · On the target application, find or
insert an outbound link, form or window.open() call pointing at it.
3 · Activate it. 4 · The probe reports whether window.opener arrived.
5 · If it did, press its demonstration button and watch the original application
tab get replaced.
Remediation — copy-paste configuration
Server header — fixes the inbound direction.
same-origin is strictest. Use same-origin-allow-popups
only where the application itself must retain handles to popups it opens, typically
an OAuth or payment window.
# nginx
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Apache (mod_headers)
Header always set Cross-Origin-Opener-Policy "same-origin"
# IIS — web.config
<httpProtocol>
<customHeaders>
<add name="Cross-Origin-Opener-Policy" value="same-origin" />
</customHeaders>
</httpProtocol>
# Express / Node
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
# Cloudflare — Rules > Transform Rules > Modify Response Header
Set static: Cross-Origin-Opener-Policy = same-origin
Markup and script — fixes the outbound direction.
Implicit noopener covers anchors only; the other three paths still
need handling:
<!-- Anchors: explicit still beats implicit --> <a href="https://third-party.example" target="_blank" rel="noopener noreferrer">…</a> <!-- window.open: implicit noopener does NOT apply here --> window.open(url, '_blank', 'noopener,noreferrer'); <!-- Forms: target="_blank" still leaks. Prefer not targeting a new tab; --> <!-- rel is valid on form elements per spec if you must. --> <form action="https://third-party.example" method="post" target="_blank" rel="noopener"> <!-- Never re-enable it explicitly --> <a href="…" target="_blank" rel="opener"> <!-- remove rel="opener" -->
Defence in depth. Pair COOP with
Cross-Origin-Embedder-Policy: require-corp for full cross-origin
isolation, and audit anywhere the application renders links somebody else supplied
— comments, profiles, uploaded documents, rich text. That is where the
rel attribute goes missing.
Independent header verification
A browser cannot read a cross-origin response header, so this tool infers COOP from behaviour. Confirm the header directly and include both — behavioural and header evidence together leave nothing to dispute:
Read the value carefully — an explicit opt-out is easy to mistake for protection:
| Value | Effect | Verdict |
|---|---|---|
same-origin |
Cross-origin openers and openees severed in both directions. | Protected |
same-origin-allow-popups |
Severs an opener holding this document, but lets the document keep handles to popups it opens. Correct for OAuth and payment flows. | Protected |
unsafe-none |
The default, stated explicitly. Handles retained — identical in effect to sending nothing. Seen in the wild on major login pages. | Not protected |
| absent | Same as unsafe-none. |
Not protected |
Because this tool judges observed behaviour rather than parsing a header,
unsafe-none and a missing header both correctly come back
vulnerable. A checker that treats "a COOP header is present" as a pass gets
this wrong.
Known limitations — read before reporting
Popup blockers. window.open() needs a user gesture.
A blocked popup yields inconclusive, never "protected" — the tool
distinguishes the two rather than guessing.
Manual tab closure. A closed handle is indistinguishable from a severed one, so the tool takes a reading at ~300 ms that no human can beat.
same-origin-allow-popups. Protects the site as an
openee but lets it keep handles to popups it opens. Mode 1 tests the
openee direction, so this reads as protected — correct, though weaker than
same-origin.
Redirects. COOP is evaluated on the final document. A target that redirects may return a different policy than its entry URL suggests. Test the landing URL directly too.
Non-browser surfaces. PDF viewers, SVG, in-app WebViews, Electron shells and email clients do not always follow modern defaults. This tool measures the browser you run it in, and nothing else.