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;
}
}
});
}
})();