Use of Web Link to Untrusted Target with window.opener Access

Description

Use of Web Link to Untrusted Target with window.opener Access occurs when a web application creates links to external websites without preventing the target page from accessing the originating page through the window.opener JavaScript property. When a user clicks a link that opens in a new tab or window (using target="_blank"), the new page gains access to the opener's window object, potentially allowing malicious external sites to redirect the original page to a phishing site or manipulate its content. This vulnerability is commonly known as "tabnabbing" or "reverse tabnabbing."

Risk

This vulnerability enables sophisticated phishing attacks. The external malicious page can silently redirect the original tab to a fake login page while the user is focused on the new tab. When the user returns to the original tab, they see what appears to be a legitimate login page requesting credentials. The attack is particularly effective because users trust that the original tab hasn't changed. Attackers can also use the opener reference to detect the original site's URL and customize their phishing page accordingly. The risk increases when linking to user-generated content or third-party sites.

Solution

Add rel="noopener noreferrer" to all anchor tags that use target="_blank". The "noopener" attribute prevents the new page from accessing window.opener. The "noreferrer" attribute additionally prevents sending the Referrer header to the external site. Modern browsers have started to implicitly add noopener for target="_blank" links, but explicit declaration ensures protection across all browser versions. For programmatic window opening using window.open(), set the opener to null immediately. Consider using Content Security Policy headers to restrict navigation. Implement link validation for user-submitted URLs.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Phishing attacks enabled by tabnabbing can steal user credentials and sensitive information.
IntegrityScope: Integrity

Modify Application Data - The opener window's location can be changed, potentially leading to malicious content injection.
Access ControlScope: Access Control

Bypass Protection Mechanism - User trust in the original tab is exploited to bypass normal phishing awareness.

Example Code

Vulnerable Code

<!-- Vulnerable: Links without noopener allowing tabnabbing -->
<!DOCTYPE html>
<html>
<head>
    <title>My Banking Portal</title>
</head>
<body>
    <h1>Welcome to Secure Bank</h1>

    <!-- Vulnerable: External link can access window.opener -->
    <a href="https://external-partner.com" target="_blank">
        Visit Our Partner Site
    </a>

    <!-- Vulnerable: User-generated link -->
    <a href="https://user-submitted-url.com" target="_blank">
        User's Profile Link
    </a>

    <!-- Vulnerable: Social media sharing -->
    <a href="https://twitter.com/share?url=..." target="_blank">
        Share on Twitter
    </a>
</body>
</html>

<!--
Attacker's malicious page at external-partner.com:
<script>
if (window.opener) {
    // Redirect the bank's tab to a phishing page
    window.opener.location = "https://attacker.com/fake-bank-login";
}
</script>
-->
// Vulnerable: Programmatic window opening without protection
function vulnerableOpenLink(url) {
    // Vulnerable: New window has access to opener
    window.open(url, '_blank');

    // The opened page can now do:
    // window.opener.location = "https://phishing-site.com"
}

// Vulnerable: Dynamic link creation
function vulnerableCreateShareLink(url) {
    const link = document.createElement('a');
    link.href = url;
    link.target = '_blank';
    // Missing rel="noopener noreferrer"
    link.textContent = 'Share';
    document.body.appendChild(link);
}
// Vulnerable: React component with unsafe external links
function VulnerableExternalLinks({ links }) {
    return (
        <div>
            {links.map(link => (
                // Vulnerable: Missing rel attribute
                <a key={link.id} href={link.url} target="_blank">
                    {link.title}
                </a>
            ))}
        </div>
    );
}

// Vulnerable: Opening URLs from user input
function VulnerableUserProfile({ website }) {
    return (
        <a href={website} target="_blank">
            Visit Website
        </a>
    );
}
<!-- Vulnerable: PHP generating links without protection -->
<?php
function vulnerableRenderLinks($links) {
    foreach ($links as $link) {
        // Vulnerable: Missing rel="noopener noreferrer"
        echo '<a href="' . htmlspecialchars($link['url']) . '" target="_blank">';
        echo htmlspecialchars($link['title']);
        echo '</a>';
    }
}

// User-submitted URLs are especially dangerous
function vulnerableRenderUserLink($userUrl) {
    echo '<a href="' . htmlspecialchars($userUrl) . '" target="_blank">';
    echo 'Visit User Site';
    echo '</a>';
}
?>

Fixed Code

<!-- Fixed: Links with noopener noreferrer -->
<!DOCTYPE html>
<html>
<head>
    <title>My Banking Portal</title>
</head>
<body>
    <h1>Welcome to Secure Bank</h1>

    <!-- Fixed: rel="noopener noreferrer" prevents tabnabbing -->
    <a href="https://external-partner.com" target="_blank" rel="noopener noreferrer">
        Visit Our Partner Site
    </a>

    <!-- Fixed: User-generated links protected -->
    <a href="https://user-submitted-url.com" target="_blank" rel="noopener noreferrer">
        User's Profile Link
    </a>

    <!-- Fixed: Social sharing protected -->
    <a href="https://twitter.com/share?url=..." target="_blank" rel="noopener noreferrer">
        Share on Twitter
    </a>
</body>
</html>
// Fixed: Programmatic window opening with protection
function fixedOpenLink(url) {
    // Fixed: Open window and immediately nullify opener
    const newWindow = window.open(url, '_blank');
    if (newWindow) {
        newWindow.opener = null;
    }
}

// Alternative: Use noopener feature
function fixedOpenLinkAlternative(url) {
    // Fixed: Use noopener in window features
    window.open(url, '_blank', 'noopener,noreferrer');
}

// Fixed: Dynamic link creation with protection
function fixedCreateShareLink(url) {
    const link = document.createElement('a');
    link.href = url;
    link.target = '_blank';
    link.rel = 'noopener noreferrer';  // Fixed: Add rel attribute
    link.textContent = 'Share';
    document.body.appendChild(link);
}

// Fixed: Utility function for safe external links
class SafeExternalLinks {
    static open(url, options = {}) {
        // Validate URL
        if (!this.isValidUrl(url)) {
            console.error('Invalid URL');
            return null;
        }

        // Check if URL is on blocklist
        if (this.isBlockedDomain(url)) {
            console.error('Blocked domain');
            return null;
        }

        // Open safely
        const features = options.features || 'noopener,noreferrer';
        return window.open(url, '_blank', features);
    }

    static isValidUrl(url) {
        try {
            new URL(url);
            return true;
        } catch {
            return false;
        }
    }

    static isBlockedDomain(url) {
        const blocklist = ['malicious-site.com', 'phishing.example'];
        const domain = new URL(url).hostname;
        return blocklist.some(blocked => domain.includes(blocked));
    }
}
// Fixed: React component with safe external links
function FixedExternalLinks({ links }) {
    return (
        <div>
            {links.map(link => (
                // Fixed: Always include rel="noopener noreferrer"
                <a
                    key={link.id}
                    href={link.url}
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    {link.title}
                </a>
            ))}
        </div>
    );
}

// Fixed: Reusable safe external link component
function SafeExternalLink({ href, children, ...props }) {
    // Fixed: Always add protection for external links
    const isExternal = href && (
        href.startsWith('http://') ||
        href.startsWith('https://') ||
        href.startsWith('//')
    );

    const relValue = isExternal ? 'noopener noreferrer' : undefined;
    const targetValue = isExternal ? '_blank' : undefined;

    return (
        <a
            href={href}
            target={targetValue}
            rel={relValue}
            {...props}
        >
            {children}
        </a>
    );
}

// Usage
function UserProfile({ website }) {
    return (
        <SafeExternalLink href={website}>
            Visit Website
        </SafeExternalLink>
    );
}
<!-- Fixed: PHP generating protected links -->
<?php
function fixedRenderLinks($links) {
    foreach ($links as $link) {
        // Fixed: Include rel="noopener noreferrer"
        echo '<a href="' . htmlspecialchars($link['url']) . '" ';
        echo 'target="_blank" rel="noopener noreferrer">';
        echo htmlspecialchars($link['title']);
        echo '</a>';
    }
}

// Fixed: Safe external link helper
function safeExternalLink($url, $text, $cssClass = '') {
    // Validate URL
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        return htmlspecialchars($text) . ' (invalid link)';
    }

    // Check protocol
    $parsed = parse_url($url);
    if (!in_array($parsed['scheme'], ['http', 'https'])) {
        return htmlspecialchars($text) . ' (invalid protocol)';
    }

    // Build safe link
    $class = $cssClass ? ' class="' . htmlspecialchars($cssClass) . '"' : '';
    return sprintf(
        '<a href="%s" target="_blank" rel="noopener noreferrer"%s>%s</a>',
        htmlspecialchars($url),
        $class,
        htmlspecialchars($text)
    );
}

// Usage
echo safeExternalLink($userUrl, 'Visit User Site', 'external-link');
?>
# Fixed: Django template tag for safe external links
from django import template
from django.utils.safestring import mark_safe
from django.utils.html import escape
from urllib.parse import urlparse

register = template.Library()

@register.simple_tag
def safe_external_link(url, text, css_class=''):
    """Generate a safe external link with noopener noreferrer"""

    # Validate URL
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ('http', 'https'):
            return escape(text) + ' (invalid link)'
    except Exception:
        return escape(text) + ' (invalid link)'

    # Build safe link
    class_attr = f' class="{escape(css_class)}"' if css_class else ''

    return mark_safe(
        f'<a href="{escape(url)}" target="_blank" '
        f'rel="noopener noreferrer"{class_attr}>{escape(text)}</a>'
    )


# Fixed: Content Security Policy middleware
class CSPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        # Add CSP header to prevent unauthorized navigation
        csp = "navigate-to 'self' https://trusted-domain.com"
        response['Content-Security-Policy'] = csp

        return response

CVE Examples

  • CVE-2016-5765: Application allowed tabnabbing through links without rel="noopener".
  • CVE-2019-11730: Browser vulnerability related to cross-origin window.opener access.

  • CWE-601: URL Redirection to Untrusted Site ('Open Redirect') (related)
  • CWE-1021: Improper Restriction of Rendered UI Layers or Frames (related)
  • CWE-346: Origin Validation Error (parent concept)

References

  1. MITRE Corporation. "CWE-1022: Use of Web Link to Untrusted Target with window.opener Access." https://cwe.mitre.org/data/definitions/1022.html
  2. OWASP. "Reverse Tabnabbing."
  3. MDN Web Docs. "Link types: noopener."