window.opener.location 安全风险讨论

上篇文章中我介绍了 Content Security Policy Level 2,之前我还介绍过 Subresource IntegrityReferrer Policy 等其它与 Web 安全或用户隐私有关的协议。由于精力有限,我所写的只是新增协议中很小的一部分,但已经可以由此感受到现代浏览器在安全上所作出的努力。然而,一个我认为比较严重的安全隐患,却始终存在于各大浏览器之中。甚至到目前为止,仍然没有一个正式标准出来指导浏览器和开发者完美解决这个问题。这让我十分困惑,我将在本文讨论这个问题。

在浏览器中,通过 <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 引用。下面是一些提议,可惜到目前为止并没有任何浏览器采纳:

  • rel="newcontext":建议给 rel 属性增加 newcontext 属性值,详情1详情2
  • rel="unrelated":建议给 rel 属性增加 unrelated 属性值,详情
  • target="_unrelated":建议给 target 属性增加 _unrelated 属性值,详情
  • disown-window-opener:建议在 CSP3 中增加 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>

点击链接后打开的确实是正常的登录页,但几秒后会被替换为提前准备好的钓鱼页,如果这时没注意地址栏的变化,就很容易被钓鱼者利用。

本文先写到这里,这个问题我会持续关注,如果有浏览器提供了更好的解决方案或者有新的标准规范出来,我会及时更新本文。也欢迎大家留言讨论。

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 424 天前,文中所描述的信息可能已发生改变,请谨慎使用。

专题「浏览器」的其他文章 »

Comments