COOP & Tabnabbing / proof of concept

Client-side only WSTG-CLNT-14

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 1

http:// or https:// only · embedded credentials rejected

seconds

⌘Ctrl+⏎ to launch
Opens the target, holds the returned handle, then assigns 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 live

                  
                

Pre-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 evidence
    A tabnabbing verdict only means something against a stated browser and version.

    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:

    Meaning of each COOP header value
    ValueEffectVerdict
    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.