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 settingwindow.openertonullin the opened window.noreferrer: Implementsnoopenerbehavior while also preventing the browser from sending theRefererheader 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.
2. Creating Centralized Link Components
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 appendsrel="noopener noreferrer". - This pattern eliminates developer oversight and standardizes outbound link behavior across the entire component tree.
3. Sanitizing User-Generated Content and Dynamic Links
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
dangerouslySetInnerHTMLorv-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-blankESLint rule flags any JSX element containingtarget="_blank"without explicitrel="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.