/ Security

Prevent Tab-Nabbing with Minimal Overhead

In this article, we're going to mitigate tab-nabbing attacks in a cross-browser way that only requires adding a single JS file/function to your application.

Tab nabbing an attack where a page linked from the target page is able to rewrite that page, for example to replace it with a phishing site.
OWASP

First things first, if you aren't familiar with tab-nabbing, I highly reccomend checking out this excellent explaination/demo by Mathias Bynens.

For some of us with a large code-base full of hard-coded links or user-generated content, this can be pretty annoying to fix everywhere. Luckily, by intercepting mouse-click events, we can check for vulnerable anchor tags and add rel=noopener if the origin is different than our own.

jQuery Example

// note: a.origin doesn't work in IE! 
// you'll need to use a polyfill or just add `rel=noopener` to all links with `target=_blank`

$(function() {
     $("body").on("click", "a[target='_blank']", function(e) {
         var link = e.currentTarget;
         link && link.tagName.toUpperCase() == "A" && link.origin != window.location.origin && link.setAttribute("rel", "noopener")
     })
  });

PureJS Example

(function() {
    ready(function() {
        preventTabNabbing();
    });
    
    // whitelistfn(element) is a function that returns true if we should allow access to `window.opener` for this link
    // 
    function preventTabNabbing(whitelistFn) {

        whitelistFn = whitelistFn || function(e) {
            return false;
        };

        delegate(document.body, "click", isTabNabbable, function(e, target) {
            if (whitelistFn(target)) return;
            target.setAttribute("rel", [(target.getAttribute("rel") || ""), "noopener"].join(" "));
        })
    }
    
    function isTabNabbable(e) {
        return e.tagName.toUpperCase() === "A" &&
            (/\b_blank\b/i).exec(e.getAttribute("target")) &&
            !(/\bnoopener\b/i).exec(e.getAttribute("rel"))
    }
    
    function addEventListener(obj, evt, fn) {
        if (obj.addEventListener) {
            obj.addEventListener(evt, fn, false);
            return true;
        } else if (obj.attachEvent) {
            return obj.attachEvent('on' + evt, fn);
        }
    }

    function ready(callback) {
        // in case the document is already rendered
        if (document.readyState != 'loading') callback();
        // modern browsers
        else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
        // IE <= 8
        else document.attachEvent('onreadystatechange', function() {
            if (document.readyState == 'complete') callback();
        });
    }


    // like jquery delegation, based on https://stackoverflow.com/a/25248515
    function delegate(baseElement, eventType, selectorFn, cb) {
        addEventListener(baseElement, eventType, function(e) {
            for (var target = e.target; target && target != this; target = target.parentNode) {
                if (selectorFn(target)) {
                    cb(e, target);
                    break;
                }
            }
        });
    }
})();