├── .gitignore ├── bower.json ├── package.json ├── gulpfile.js ├── LICENSE ├── blankshield.min.js ├── README.md └── blankshield.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | coverage 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | node_modules 17 | 18 | .grunt 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blankshield", 3 | "version": "0.6.0", 4 | "description": "Prevent reverse tabnabbing phishing attacks caused by _blank", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Daniel St. Jules", 9 | "email": "danielst.jules@gmail.com", 10 | "url": "http://danielstjules.com" 11 | } 12 | ], 13 | "keywords": [ 14 | "prevent", 15 | "reverse", 16 | "tabnabbing", 17 | "target", 18 | "_blank", 19 | "security" 20 | ], 21 | "ignore": [ 22 | ".*", 23 | "gulpfile.js", 24 | "package.json", 25 | "README.md", 26 | "demo" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blankshield", 3 | "version": "0.6.3", 4 | "description": "Prevent reverse tabnabbing phishing attacks caused by _blank", 5 | "keywords": [ 6 | "prevent", 7 | "reverse", 8 | "tabnabbing", 9 | "target", 10 | "_blank", 11 | "security" 12 | ], 13 | "author": "Daniel St. Jules ", 14 | "license": "MIT", 15 | "homepage": "https://github.com/danielstjules/blankshield", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/danielstjules/blankshield.git" 19 | }, 20 | "main": "blankshield.js", 21 | "devDependencies": { 22 | "gulp": "*", 23 | "gulp-rename": "^1.2.0", 24 | "gulp-uglify": "^0.3.1", 25 | "gulp-header": "^1.1.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var uglify = require('gulp-uglify'); 3 | var rename = require('gulp-rename'); 4 | var header = require('gulp-header'); 5 | 6 | var pkg = require('./package.json'); 7 | var banner = [ 8 | '/**', 9 | ' * <%= pkg.name %> - <%= pkg.description %>', 10 | ' *', 11 | ' * @version <%= pkg.version %>', 12 | ' * @link <%= pkg.homepage %>', 13 | ' * @author <%= pkg.author %>', 14 | ' * @license <%= pkg.license %>', 15 | ' */\n' 16 | ].join('\n'); 17 | 18 | var paths = { 19 | scripts: './blankshield.js', 20 | dist: './' 21 | }; 22 | 23 | gulp.task('minify', function() { 24 | gulp.src(paths.scripts) 25 | .pipe(uglify()) 26 | .pipe(header(banner, {pkg: pkg})) 27 | .pipe(rename(function(path) { 28 | path.basename += '.min'; 29 | })) 30 | .pipe(gulp.dest(paths.dist)); 31 | }); 32 | 33 | gulp.task('dist', ['minify']); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel St. Jules 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /blankshield.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * blankshield - Prevent reverse tabnabbing phishing attacks caused by _blank 3 | * 4 | * @version 0.6.3 5 | * @link https://github.com/danielstjules/blankshield 6 | * @author Daniel St. Jules 7 | * @license MIT 8 | */ 9 | !function(e){"use strict";function n(e){if("undefined"==typeof e.length)o(e,"click",t);else if("string"!=typeof e&&!(e instanceof String))for(var n=0;n 56 | 57 | Browser 58 | Click 59 | Shift + click 60 | Meta/Ctrl + click 61 | 62 | 63 | Chrome 40 64 | x 65 | x 66 | x 67 | 68 | 69 | Firefox 34 70 | 71 | 72 | 73 | 74 | 75 | Opera 26 76 | x 77 | x 78 | x 79 | 80 | 81 | Safari 7, 8 82 | x 83 | 84 | 85 | 86 | 87 | IE6...11 88 | [1] 89 | 90 | 91 | 92 | [1] IE is not vulnerable to the attack by default. However, this can 93 | change depending on security zone settings. 94 | 95 | ## Installation 96 | 97 | The library can be installed via npm: 98 | 99 | ``` bash 100 | npm install --save blankshield 101 | ``` 102 | 103 | Or using bower: 104 | 105 | ``` bash 106 | bower install blankshield 107 | ``` 108 | 109 | ## Usage 110 | 111 | blankshield.js works in global, CommonJS and AMD contexts. 112 | 113 | #### blankshield(target) 114 | 115 | blankshield is the main function exported by the library. It accepts an 116 | anchor element or array of elements, adding an event listener to each to 117 | help mitigate a potential reverse tabnabbing attack. For performance, any 118 | supplied object with a length attribute is assumed to be an array. 119 | 120 | ``` JavaScript 121 | // It works on a single element 122 | blankshield(document.getElementById('some-anchor')); 123 | 124 | // Array-like objects such as HTMLCollections 125 | blankshield(document.getElementsByClassName('user-submitted-link')); 126 | blankshield(document.getElementsByTagName('a')); 127 | blankshield(document.querySelectorAll('a[target=_blank]')); 128 | 129 | // As well as jQuery 130 | blankshield($('a[target=_blank]')); 131 | 132 | // But make sure not to bind listeners to the anchors that would stop event 133 | // propagation. In the example below, blankshield is not able to intercept the 134 | // click behavior. 135 | var anchor = document.getElementById('some-anchor') 136 | anchor.addEventListener('click', function(e) { 137 | e.stopImmediatePropagation(); 138 | }); 139 | blankshield(document.getElementById('some-anchor')); 140 | ``` 141 | 142 | #### blankshield.open(strUrl, \[strWindowName\], \[strWindowFeatures\]) 143 | 144 | Accepts the same arguments as window.open. If the strWindowName is not 145 | equal to one of the safe targets (_top, _self or _parent), then it opens 146 | the destination url using "window.open" from an injected iframe, then 147 | removes the iframe. This behavior applies to all browsers except IE < 11, 148 | which use "window.open" followed by setting the child window's opener to 149 | null. If the strWindowName is set to some other value, the url is simply 150 | opened with window.open(). 151 | 152 | ``` JavaScript 153 | // To open an url with blankshield, instead of window.open() 154 | blankshield.open('https://www.github.com/danielstjules'); 155 | 156 | // To bind a listener using jQuery with event delegation 157 | // (Assumes no other listeners prevent propagation) 158 | $('body').on('click', 'a[target=_blank]', function(event) { 159 | var href = $(this).attr('href'); 160 | blankshield.open(href); 161 | event.preventDefault(); 162 | }); 163 | ``` 164 | 165 | #### blankshield.patch() 166 | 167 | Patches window.open() to use blankshield.open() for _blank targets. 168 | 169 | ``` JavaScript 170 | blankshield.patch(); 171 | ``` 172 | 173 | ## Solutions 174 | 175 | A handful of solutions exist to prevent this sort of attack. You could: 176 | 177 | * Remove or disallow `target="_blank"` for any anchors pointing to a 178 | different origin. 179 | * Append `rel="noreferrer"` to any links with `target="_blank"`. When done, 180 | `window.opener` will be null from the child window. It's well supported among 181 | webkit-based browsers, though you'll fall short with IE and Safari. And of 182 | course, it prevents sending the referrer in the request headers. You could 183 | fall off as an identifiable source of traffic for some friendly sites. 184 | * Append `rel="noopener"` to any links with `target="_blank"`. When done, 185 | `window.opener` will be null from the child window. See 186 | [caniuse](http://caniuse.com/#feat=rel-noopener) for current browser support. 187 | * Listen for the click event and prevent the default browser behavior of 188 | opening a new tab. Then, call `window.open()` with the href and set the 189 | the child's opener to null. Unfortunately, this does not work for Safari. 190 | Safari's cross-origin security prevents the modification of `window.opener` of a 191 | child window if it lies on a different origin, yet still allows the child 192 | window to access `window.opener.location`. 193 | * Listen for the click event and prevent the default browser behavior of 194 | opening a new tab. Inject a hidden iframe that opens the new tab, then 195 | immediately remove the iframe. This is what blankshield does. 196 | -------------------------------------------------------------------------------- /blankshield.js: -------------------------------------------------------------------------------- 1 | ;(function(root) { 2 | 'use strict'; 3 | 4 | /** 5 | * Detect IE versions older than 11. 6 | * 7 | * @var {boolean} 8 | */ 9 | var oldIE; 10 | if (typeof window !== 'undefined') { 11 | oldIE = window.navigator.userAgent.indexOf('MSIE') !== -1; 12 | } 13 | 14 | /** 15 | * Cached window.open function. 16 | * 17 | * @var {function} 18 | */ 19 | var open; 20 | if (typeof window !== 'undefined') { 21 | open = window.open; 22 | } 23 | 24 | /** 25 | * blankshield is the main function exported by the library. It accepts an 26 | * anchor element or array of elements, adding an event listener to each to 27 | * help mitigate a potential reverse tabnabbing attack. For performance, any 28 | * supplied object with a length attribute is assumed to be an array. 29 | * 30 | * @param {HTMLAnchorElement|HTMLAnchorElement[]} target 31 | */ 32 | function blankshield(target) { 33 | if (typeof target.length === 'undefined') { 34 | addEventListener(target, 'click', clickListener); 35 | } else if (typeof target !== 'string' && !(target instanceof String)) { 36 | for (var i = 0; i < target.length; i++) { 37 | addEventListener(target[i], 'click', clickListener); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Accepts the same arguments as window.open. If the strWindowName is not 44 | * equal to one of the safe targets (_top, _self or _parent), then it opens 45 | * the destination url using "window.open" from an injected iframe, then 46 | * removes the iframe. This behavior applies to all browsers except IE < 11, 47 | * which use "window.open" followed by setting the child window's opener to 48 | * null. If the strWindowName is set to some other value, the url is simply 49 | * opened with window.open(). 50 | * 51 | * @param {string} strUrl 52 | * @param {string} [strWindowName] 53 | * @param {string} [strWindowFeatures] 54 | * @returns {Window} 55 | */ 56 | blankshield.open = function(strUrl, strWindowName, strWindowFeatures) { 57 | var child; 58 | 59 | if (safeTarget(strWindowName)) { 60 | return open.apply(window, arguments); 61 | } else if (!oldIE) { 62 | return iframeOpen(strUrl, strWindowName, strWindowFeatures); 63 | } else { 64 | // Replace child.opener for old IE to avoid appendChild errors 65 | // We do it for all to avoid having to sniff for specific versions 66 | child = open.apply(window, arguments); 67 | if (child) { 68 | child.opener = null; 69 | } 70 | return child; 71 | } 72 | }; 73 | 74 | /** 75 | * Patches window.open() to use blankshield.open() for new window/tab targets. 76 | */ 77 | blankshield.patch = function() { 78 | window.open = function() { 79 | return blankshield.open.apply(this, arguments); 80 | }; 81 | }; 82 | 83 | /** 84 | * An event listener that can be attached to a click event to protect against 85 | * reverse tabnabbing. It retrieves the target anchors href, and if the link 86 | * was intended to open in a new tab or window, the browser's default 87 | * behavior is canceled. Instead, the destination url is opened using 88 | * "window.open" from an injected iframe, and the iframe is removed. Except 89 | * for IE < 11, which uses "window.open" followed by setting the child 90 | * window's opener to null. 91 | * 92 | * @param {Event} e The click event for a given anchor 93 | */ 94 | function clickListener(e) { 95 | var target, targetName, href, usedModifier; 96 | 97 | // Use global event object for IE8 and below to get target 98 | e = e || window.event; 99 | // Won't work for IE8 and below for cases when e.srcElement 100 | // refers not to the anchor, but to the element inside it e.g. an image 101 | target = e.currentTarget || e.srcElement; 102 | 103 | // Ignore anchors without an href 104 | href = target.getAttribute('href'); 105 | if (!href) return; 106 | 107 | // Ignore anchors without an unsafe target or modifier key 108 | usedModifier = (e.ctrlKey || e.shiftKey || e.metaKey); 109 | targetName = target.getAttribute('target'); 110 | if (!usedModifier && (!targetName || safeTarget(targetName))) { 111 | return; 112 | } 113 | 114 | blankshield.open(href); 115 | 116 | // IE8 and below don't support preventDefault 117 | if (e.preventDefault) { 118 | e.preventDefault(); 119 | } else { 120 | e.returnValue = false; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | /** 127 | * A cross-browser addEventListener function that adds a listener for the 128 | * supplied event type to the specified target. 129 | * 130 | * @param {object} target 131 | * @param {string} type 132 | * @param {function} listener 133 | */ 134 | function addEventListener(target, type, listener) { 135 | var onType, prevListener; 136 | 137 | // Modern browsers 138 | if (target.addEventListener) { 139 | return target.addEventListener(type, listener, false); 140 | } 141 | 142 | // Older browsers 143 | onType = 'on' + type; 144 | if (target.attachEvent) { 145 | target.attachEvent(onType, listener); 146 | } else if (target[onType]) { 147 | prevListener = target[onType]; 148 | target[onType] = function() { 149 | listener(); 150 | prevListener(); 151 | }; 152 | } else { 153 | target[onType] = listener; 154 | } 155 | } 156 | 157 | /** 158 | * Opens the provided url by injecting a hidden iframe that calls 159 | * window.open(), then removes the iframe from the DOM. 160 | * 161 | * @param {string} url The url to open 162 | * @param {string} [strWindowName] 163 | * @param {string} [strWindowFeatures] 164 | * @returns {Window} 165 | */ 166 | function iframeOpen(url, strWindowName, strWindowFeatures) { 167 | var iframe, iframeDoc, script, openArgs, newWin; 168 | 169 | iframe = document.createElement('iframe'); 170 | iframe.style.display = 'none'; 171 | document.body.appendChild(iframe); 172 | iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 173 | 174 | openArgs = '"' + url + '"'; 175 | if (strWindowName) { 176 | openArgs += ', "' + strWindowName + '"'; 177 | } 178 | if (strWindowFeatures) { 179 | openArgs += ', "' + strWindowFeatures + '"'; 180 | } 181 | 182 | script = iframeDoc.createElement('script'); 183 | script.type = 'text/javascript'; 184 | script.text = 'window.parent = null; window.top = null;' + 185 | 'window.frameElement = null; var child = window.open(' + openArgs + ');' + 186 | 'if (child) { child.opener = null }'; 187 | iframeDoc.body.appendChild(script); 188 | newWin = iframe.contentWindow.child; 189 | 190 | document.body.removeChild(iframe); 191 | return newWin; 192 | } 193 | 194 | /** 195 | * Returns whether or not the given target is safe. 196 | * 197 | * @param {string} target 198 | * @return {boolean} 199 | */ 200 | function safeTarget(target) { 201 | return target === '_top' || target === '_self' || target === '_parent'; 202 | } 203 | 204 | /** 205 | * Export for various environments. 206 | */ 207 | 208 | // Export CommonJS 209 | if (typeof exports !== 'undefined') { 210 | if (typeof module !== 'undefined' && module.exports) { 211 | module.exports = blankshield; 212 | } else { 213 | exports.blankshield = blankshield; 214 | } 215 | } 216 | 217 | // Register with AMD 218 | if (typeof define == 'function' && typeof define.amd == 'object') { 219 | define('blankshield', [], function() { 220 | return blankshield; 221 | }); 222 | } 223 | 224 | // export default blankshield function 225 | root.blankshield = blankshield; 226 | }(this)); 227 | --------------------------------------------------------------------------------