DEV Community

Cover image for Why rel="noopener" Is Your Front Line Against Tab-Nabbing Phishing Attacks
Julian Neagu
Julian Neagu

Posted on

Why rel="noopener" Is Your Front Line Against Tab-Nabbing Phishing Attacks

TL;DR: The rel="noopener" attribute prevents tab-nabbing phishing attacks by severing the JavaScript window.opener reference between your page and any new tab opened with target="_blank". Modern browsers apply it automatically, but explicit declaration remains essential for security compliance, older browser coverage, and performance isolation.

When you add target="_blank" to a link, you're creating more than just a new tab. You're creating a JavaScript back-channel between two pages. The newly opened tab receives a reference to your original page through window.opener, and that reference can be weaponized. A malicious site can redirect your tab to a phishing page while the user is distracted. The user switches back, sees what looks like your login screen, and types in their credentials.

This isn't a theoretical vulnerability. It's a documented attack vector with a name: tab-nabbing. And the mitigation is trivially simple. Add six characters to every external link: rel="noopener".

How window.opener Creates a JavaScript Back-Channel

When you write this:

html
<a href="https://external-docs.com" target="_blank">
Read the documentation
</a>

The browser opens external-docs.com in a new tab. Nothing surprising there. But behind the scenes, the new tab gets a JavaScript object called window.opener that points back to your original page.

That reference is live. The external page can't read your DOM or access your cookies directly, because same-origin policy blocks cross-domain data access. But it can do one thing that breaks the entire security model: it can change where your tab points.

Without noopener, any page opened with target="_blank" can redirect your original tab to a phishing site using window.opener.location, even across completely different domains.

The external site runs this single line of JavaScript:

javascript
window.opener.location = "https://fake-login-page.com";

Your original tab, sitting in the background, now displays a convincing replica of your login page. The user doesn't notice immediately because they're focused on the new tab. When they switch back, they see "session expired" and re-enter their credentials. The attacker captures everything.

Diagram showing tab-nabbing attack flow where external site uses window.opener to redirect original tab to phishing page

The Attack Sequence

Here's how tab-nabbing plays out in practice:

  1. User is logged into your app at yourapp.com
  2. User clicks an external link that opens with target="_blank"
  3. The new tab loads, runs JavaScript that executes window.opener.location = "https://fake-yourapp.com"
  4. Your original tab (sitting in the background) is now showing a fake login page
  5. User switches back, sees "session expired", types in credentials
  6. Attacker logs the credentials and redirects to the real login page

The attack works because changing another window's location is one of the few cross-origin operations browsers historically allowed. You can't read the other window's content, but you can send it somewhere else. That's all a phishing attack needs.

The Performance Benefit: Process Isolation

Security is the main reason to use noopener, but there's a secondary benefit: performance isolation.

Without noopener, the new tab runs in the same browser process as your page. If the external site is resource-heavy (large JavaScript bundles, memory leaks, slow rendering), it can degrade your site's performance even though it's in a separate tab. Your UI might stutter. Your event loop might block.

Adding rel="noopener" allows browsers to spawn the new tab in a completely separate process, preventing external sites from degrading your original page's performance.

When you add rel="noopener", the browser can spawn the new tab in a completely separate process. That means a slow or broken external page won't jank your app's UI or consume your memory budget. This matters more on lower-end devices and mobile browsers where process isolation is less aggressive. It's not the primary reason to declare noopener, but it's a measurable side benefit.

VS Code editor showing HTML code with an anchor tag containing rel=

What rel="noopener" Actually Does

The noopener attribute does exactly one thing: it sets window.opener to null in the newly opened tab. The tab still opens. The link still works. The user experience is identical. But the new tab has no reference back to your page, which means it can't touch it.

Here's the corrected version:

html
<a href="https://external-docs.com" target="_blank" rel="noopener">
Read the documentation
</a>

That's it. Six extra characters. The link behaves the same from the user's perspective, but the security hole is closed.

If the external site is one you don't fully trust (user-generated content, third-party integrations, ads), you can also add noreferrer:

html
<a href="https://sketchy-site.com" target="_blank" rel="noopener noreferrer">
External link
</a>

The noreferrer part prevents your URL from being sent to the external site as the Referer header. That's useful for privacy (the external site won't know where the traffic came from), but it's a separate concern. For most cases, noopener alone is sufficient.

Do Modern Browsers Apply noopener Automatically?

Yes, since 2021. Chrome version 88 made the change first. Firefox and Safari followed shortly after. Edge inherited the behavior when it switched to Chromium.

If you're targeting only modern browsers, you're already protected by default. The browser treats every target="_blank" as if it had rel="noopener", whether you wrote it or not.

So why still write it explicitly?

Three Reasons to Keep Writing rel="noopener"

Older browser support. Chrome 88 was released in January 2021. Firefox 79 (released July 2020) implemented the same default. Safari added it in version 12.2 (March 2019). But users on older versions of these browsers, or on outdated mobile browsers, are still vulnerable. Explicit declaration protects everyone.

Security scanners and compliance. Tools like static code analysis checkers flag missing noopener on target="_blank" links as vulnerabilities. SOC2 audits and penetration tests will call it out. If you're working in fintech, healthcare, or enterprise SaaS, auditors expect to see explicit noopener declarations. The fact that modern browsers handle it automatically doesn't satisfy the audit checklist.

Security scanners, linters, and compliance audits flag missing noopener on target="_blank" links as vulnerabilities, making explicit declaration necessary for SOC2 and pen test compliance.

Documentation and intent. Code is read more often than it's written. When another developer sees target="_blank" without noopener, they don't know if the omission was intentional or accidental. Explicit declaration signals that the security consideration was addressed.

Split comparison showing window.opener returning object without noopener versus null with noopener attribute

Where Developers Forget to Add noopener

The most common mistake isn't forgetting to add noopener to handwritten links. It's forgetting to add it to dynamically generated links.

Links Generated from User Content

If your app renders Markdown or rich text from users, and that content includes links, you need to ensure the renderer adds noopener to every target="_blank" link. Most Markdown libraries don't do this by default.

For example, using a JavaScript Markdown library without configuring link safety:

javascript
// Unsafe: user Markdown links open without noopener
const html = marked.parse(userContent);

You need to configure the renderer:

`javascript
const renderer = new marked.Renderer();
renderer.link = (href, title, text) => {
const titleAttr = title ?
title="${title}": '';
return
${text}`;
};

const html = marked.parse(userContent, { renderer });
``

Links in Third-Party Embeds

If your app embeds content from third parties (documentation widgets, comment systems, social media feeds), you don't control the HTML those systems inject. You need to sanitize it on your end.

One approach: walk the DOM after the embed loads and add noopener to any target="_blank" link that's missing it.

javascript
document.querySelectorAll('a[target="_blank"]:not([rel~="noopener"])').forEach(link => {
const currentRel = link.getAttribute('rel') || '';
link.setAttribute('rel', `${currentRel} noopener`.trim());
});

This catches external content you didn't write but are responsible for serving.

Is the Risk Actually High?

The probability of a tab-nabbing attack is low. Most external sites you link to are not malicious. Most users don't click links to sketchy domains from within your app. And modern browsers already mitigate the issue automatically.

But the impact is high. A successful tab-nabbing attack can compromise user credentials, leading to account takeover, data exfiltration, and reputational damage. The cost of mitigation is negligible: six extra characters. The risk is low-probability, high-severity. That math always favors the fix.

From a strategic security perspective, noopener is a defense-in-depth measure. It's not your only layer of protection, but it's a layer that costs nothing to add and closes a documented attack vector.

Chrome DevTools Elements panel showing HTML anchor tag with rel=

How to Audit Your Codebase for Missing noopener

If you're working on an existing codebase and want to find every external link that's missing noopener, here's the audit process:

Step 1: Search for target="_blank"

Grep your codebase for every occurrence of target="_blank":

On macOS or Linux:

bash
grep -r 'target="_blank"' ./src

On Windows PowerShell:

powershell
Select-String -Path .\src\* -Pattern 'target="_blank"' -Recurse

This gives you a list of every file and line where a new-tab link exists.

Step 2: Check for Missing noopener

For each result, check whether the same line contains rel="noopener" or rel="noopener noreferrer". If it doesn't, add it.

You can automate this with a more specific grep:

On macOS or Linux:

bash
grep -r 'target="_blank"' ./src | grep -v 'noopener'

On Windows PowerShell:

powershell
Select-String -Path .\src\* -Pattern 'target="_blank"' -Recurse | Where-Object { $_.Line -notmatch 'noopener' }

Any result from this search is a link that opens a new tab without noopener.

Step 3: Add Linter Rules

To prevent future regressions, add an ESLint rule that flags missing noopener:

json
{
"rules": {
"react/jsx-no-target-blank": ["error", { "enforceDynamicLinks": "always" }]
}
}

This rule enforces noopener on every target="_blank" link, including dynamically generated ones. It fails the build if the attribute is missing.

Step 4: Automate in CI

Add a script to your CI pipeline that runs the grep check and fails the build if any target="_blank" link is missing noopener. This makes the mitigation enforceable at the infrastructure level, not just at the code-review level.

The Bottom Line

The rel="noopener" attribute is a one-line fix for a documented security vulnerability. Modern browsers apply it automatically, but explicit declaration remains necessary for compliance, older browser support, and defense-in-depth strategy.

If you're writing new links, add noopener to every target="_blank" link. If you're auditing an existing codebase, search for target="_blank", check for missing noopener, and add linter rules to prevent regressions. If you're rendering user-generated content or third-party embeds, sanitize the output to ensure every external link includes noopener.

The cost is six characters. The risk is account takeover. The fix is trivial. Ship it.


📦 Publishing Kit — Dev.to

Title Options (5)

Selected: Why rel="noopener" Is Your Front Line Against Tab-Nabbing Phishing Attacks

Alternates:

  1. The Security Vulnerability Hiding in Every target="_blank" Link
  2. How window.opener Turns Your External Links Into Phishing Attack Vectors
  3. rel="noopener" Explained: Stop Tab-Nabbing Attacks With Six Characters
  4. The JavaScript Back-Channel You Created By Forgetting rel="noopener"

Slug

why-rel-noopener-prevents-tab-nabbing-phishing-attacks

Tags

webdev, security, javascript, performance

Top comments (0)