在浏览器中,通过 <a>
标签或者 JavaScript 中的 window.open
函数,可以打开新页面。新页面的 window
对象中,存在一个 opener
属性,保存对父页面的引用。我们知道,Web 应用的安全性,很大程度上是由同源策略(Same Origin
Policy,SOP)所保证的。但是,在子页面访问 opener.location
的一些属性和方法时却不受 SOP
保护,这就是本文要探讨问题的核心所在。
来看一个案例,假设父页面中有新窗口打开的子页面链接:
<a href="https://qgy18.com/file/opener.html" target="_blank">click me</a>
opener.html
中有这样一段代码:
<script>
window.opener.location = 'https://imququ.com/post/about.html';
//window.opener.location.replace('https://imququ.com/post/about.html');
</script>
将以上两段代码,分别生成两个不同域的页面(本文探讨安全风险,故只考虑不同域的情况)。在大部分浏览器中,通过父页面中的链接打开子页面后,子页面都可以通过
opener.location
将父页面跳走(上面两行 JS 可以都可以跳转,不同之处是 replace
不产生历史纪录)。
现在很多社区允许用户填写个人网站链接。设想一下,你点开某人资料中的链接,浏览一番后关掉新窗口,如果原来的页面已经被重定向到高仿的钓鱼页,你会轻易察觉出来么?
这个现象,很早之前就被人发现并利用在黑帽 SEO 上,同样很早之前,就有人给各大浏览器提 bug(详情),得到的建议无外乎两种:1)通过 window.open 打开链接,并将 opener 置为空;2)通过给链接加上 rel=noreferrer 属性,将 opener 置为空。
我们测试一下这两种方案是否能达到预期效果,以及是否会带来负面影响。加上默认情况,一共要测试三种情况,代码如下:
<a href="https://qgy18.com/file/opener.html" target="_blank">click me</a>
<a href="https://qgy18.com/file/opener.html" target="_blank" onclick="var win=window.open(this.href,'_blank');win.opener=null;return false;">click me</a>
<a href="https://qgy18.com/file/opener.html" target="_blank" rel="noreferrer">click me</a>
以下是我在部分浏览器下的测试结果:
浏览器 1)默认情况 2)window.opener=null 3)rel=noreferrer
IE 8.0.6001.18702
不跳转 *,有 Referrer
不跳转,无 Referrer
不跳转 *,有 Referrer
IE 11.0.10240.16431
跳转,有 Referrer
不跳转,无 Referrer
跳转,有 Referrer
Edge 20.10240.16384.0
跳转,有 Referrer
不跳转,无 Referrer
跳转,有 Referrer
Chrome 45.0.2454.101
跳转,有 Referrer
不跳转,有 Referrer
不跳转,无 Referrer
Firefox 41.0.1
跳转,有 Referrer
不跳转,有 Referrer
不跳转,无 Referrer
Safari 9.0.1
跳转,有 Referrer
跳转,有 Referrer
不跳转,无 Referrer
(注:IE 8.0 中,方案 1 和 3 默认不会跳走,但会有弹出窗口被拦截的提示。这个问题可以通过在页面增加 var location;
来解决,不属于本文重点,这里不展开讨论)
由表格可以看出,在所有现代浏览器中,默认情况下父页面都会被跳走。方案 1,在最新的 Safari 下不能阻止跳转,并且会导致 IE 系列丢失 Referrer;方案 2,在不支持 rel=noreferrer 的 IE 中等同于默认情况,在其它浏览器中可以阻止跳转,同时 Referrer 也被去掉了。
这两个方案都不完美,Referrer 在很多时候并不能轻易去掉,这样只剩下 window.open 这个「改动成本大、不优雅、会引入新的问题」的方案勉强可用了。
于是,一些人开始提出各种建议,试图让浏览器既能保留 Referrer,又能阻断 opener 引用。下面是一些提议,可惜到目前为止并没有任何浏览器采纳:
newcontext
属性值,详情1、详情2;unrelated
属性值,详情;_unrelated
属性值,详情;disown-window-opener
指令,详情;2016-05-11 更新,目前有一个新的 rel
属性来解决这个问题:
rel=noopener, Ensure new browsing contexts are opened without a useful window.opener
给 A 链接加上这个属性,打开的新页面再也无法通过 window.opener
获取父页面,同时 Referer
不受影响,可以看作这个问题的终极解决方案了。这个属性更多描述详见 HTML Standard;浏览器支持情况详见 CanIUse,当前只有 Chrome
49+ 支持。
到这里为止,我们讨论的都是「新窗口打开的子页面将父页面跳走」所带来的风险。实际上,父页面也可以将子页面跳走,这也是一个风险点。假设我的网站上有一个名为「XX 网站登录」的外链,用户点击后发现打开的确实是 XX 网站登录页,正准备输入密码时父页面将这个子页面跳转到钓鱼页面,也不容易被察觉。为了避免加载时的空白,还可以将钓鱼页以 data URIs 的形式编码,事先准备好。
下面是一个简单的案例:
<a id="link" href="#" target="_blank">点击打开 XXX 网站</a>
<script>
document.getElementById('link').addEventListener('click', function(e) {
e.preventDefault();
var win = window.open('https://qgy18.com/file/login.html', '_blank');
setTimeout(function() {
win.location.replace('data:text/html;charset=utf-8,<!DOCTYPE%20html><html><head><meta%20charset%3D"utf-8"%20%2F><%2Fhead><body><div>这是虚假的登录页面:<br><br><input><%2Fdiv><%2Fbody><%2Fhtml>');
}, 3000);
});
</script>
点击链接后打开的确实是正常的登录页,但几秒后会被替换为提前准备好的钓鱼页,如果这时没注意地址栏的变化,就很容易被钓鱼者利用。
本文先写到这里,这个问题我会持续关注,如果有浏览器提供了更好的解决方案或者有新的标准规范出来,我会及时更新本文。也欢迎大家留言讨论。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8