How to Stop Tabnabbing in Single-Page Applications

Tabnabbing is a phishing attack where a newly opened browser tab maliciously redirects the original parent tab to a fraudulent webpage using the window.opener API. In single-page applications (SPAs), where user sessions are often persistent and dynamic content rendering is common, tabnabbing poses a serious security risk. Web development teams secure SPAs against these attacks through modern HTML attributes, framework-level component abstraction, rigorous sanitization of user-generated content, automated linting, and modern HTTP isolation headers.

The Mechanism of Tabnabbing

When an application opens an external link using <a href="..." target="_blank">, the target page gains a reference to the source page via window.opener. An attacker controlling the destination site can execute JavaScript such as window.opener.location = 'https://fake-login-page.com'. Because the user is distracted by the new tab, they often do not notice that their original SPA tab has been navigated to an identical phishing interface designed to harvest credentials or tokens.

1. Enforcing rel="noopener noreferrer"

The primary defense against tabnabbing is the rel attribute. Development teams append rel="noopener noreferrer" to any link that includes target="_blank":

  • noopener: Completely severs the link between the pages by setting window.opener to null in the opened window.
  • noreferrer: Implements noopener behavior while also preventing the browser from sending the Referer header to the destination URL, offering defense-in-depth for older browsers.

While modern Chromium, Firefox, and WebKit browsers now treat target="_blank" as implicitly including rel="noopener" by default, relying solely on browser defaults leaves legacy browsers and specific webviews vulnerable. Explicit declaration remains a development best practice.

In SPAs built with React, Vue, or Angular, teams avoid writing raw <a> tags with target="_blank". Instead, they enforce the use of centralized, custom link components that automatically enforce secure attributes.

For example, a shared link component inspects outgoing destinations:

  • If the URL points to an external domain and contains target="_blank", the component automatically appends rel="noopener noreferrer".
  • This pattern eliminates developer oversight and standardizes outbound link behavior across the entire component tree.

SPAs frequently render markdown, comments, or external URLs provided by users via APIs. If users can inject raw HTML or Markdown links, they can introduce vulnerable target="_blank" tags.

Teams counter this by:

  • Using HTML sanitizers like DOMPurify before injecting content via properties such as dangerouslySetInnerHTML or v-html.
  • Configuring the sanitizer to automatically add rel="noopener noreferrer" to all links or strip out dangerous attributes entirely.

4. Deploying Cross-Origin-Opener-Policy (COOP)

To apply defense-in-depth at the network layer, teams configure web servers and CDN edges to deliver the Cross-Origin-Opener-Policy (COOP) HTTP response header:

Cross-Origin-Opener-Policy: same-origin

This header isolates the SPA's top-level document from other browsing contexts. Even if an external link is opened without rel="noopener", the browser places the new tab in a separate browsing context group, preventing the new document from interacting with the SPA via window.opener.

5. Automated CI/CD and Linting Checks

Human error is the most common cause of tabnabbing vulnerabilities. Development teams integrate static analysis rules into their build pipelines to detect insecure links before code reaches production.

  • In React projects, the react/jsx-no-target-blank ESLint rule flags any JSX element containing target="_blank" without explicit rel="noreferrer noopener".
  • Similar linters exist for Vue (vue/no-template-target-blank) and Angular templates, failing builds automatically whenever an insecure anchor tag is detected.