Chrome 将默认屏蔽“非用户行为触发”的父页面跳转
iframe 中的页面和它的父页面如果是跨域了的,会受同源策略限制,不能互相访问。比如在 iframe 中执行
alert(top.location.href)
, 就会抛出异常,具体到 HTML 规范中,是这样规定的:
The href attribute's getter must run these steps:
1. If this Location object's relevant Document's origin is not same origin-domain with the entry settings object's origin, then throw a "SecurityError" DOMException.
2. Return this Location object's url, serialized.
也就是说,你无法知道别人页面的 URL。但是,很多人不知道的是,虽然
href
属性不能跨域读取,却可以跨域更改:
The href attribute's setter must run these steps:
1. Parse the given value relative to the entry settings object. If that failed, throw a TypeError exception.
2. Location-object-setter navigate to the resulting URL record.
The href attribute setter intentionally has no security check.
更改
href
属性的效果大家都知道了,就是会产生跳页,想跳到哪里就跳到哪里。
通过 top.location = ... 跳页有哪些正面的使用场景?
1. framebusting
大家应该都知道点击劫持(clickjacking)吧,点击劫持是靠把被攻击页面放在一个透明度为 0(或者接近 0)的 iframe 里来实现攻击的。很多年前,页面们为了防止自己被嵌入到 iframe 里(在国内还可能是被运营商劫持。。。),都在自己页面的 JS 里加了诸如:
if (top !== self) {
top.location = location
}
这样的代码,也就是如果发现自己不在顶层窗口,就把顶层窗口的网址改成自己,就这样突破了 iframe 的嵌入。bust 是打破、打碎的意思,framebusting 就专门用来指代这种突破 iframe 嵌入的行为。
但。。。自从 HTML 5 为
<iframe>
标签新增了
sandbox
属性以来,这种防御措施就失效了。攻击者只要为 iframe 添加了
sandbox
属性,比如
<iframe src="..." sandbox="allow-script">
,被攻击页面内部就没有权限再修改父页面的网址了,除非攻击者额外添加了
sandbox="allow-top-navigation"
指令,当然这是不可能的。
有朋友就问了,那这
sandbox
怎么帮坏人啊?当然不是的,
sandbox
有它自己的正规用途,就是当父页面是好页面,担心 iframe 中的子页面做坏事就会用到这个属性了。
现代浏览器中,有更好的方式来做 framebusting,那就是
X-Frame-Options
响应头以及更先进的 CSP 中的
frame-ancestors
指令,不展开讲了。也就是说,使用
top.location =
来防御点击劫持的使用场景已经不存在了。
2. 公共登录框
在大厂里面,有很多不同域名的网站,但都共用一个账号体系,这就就产生了公共登录框。比如阿里系的网站,很多都是用 iframe 嵌入了淘宝的登录框,比如天猫的登录页面里就用了
<iframe src="//login.taobao.com/member/login.jhtml"></iframe>
用户登录之后这个登录框会通过
top.location =
来让父页面跳转到合适的页面地址。当然,现在淘宝登录框已经不用这样的代码了,改成了
postMessage
的方式,什么时候改的我下面会讲到。
利用 top.location = ... 跳页来干坏事
除了上面提到的这两个,肯定还有一些我不知道的正面使用场景。但除了这些正面案例,也有人利用
top.location =
干坏事 ,比如恶意广告。
现在使用 iframe 最多的场景就是广告,一些恶意广告会通过
top.location =
把用户想要浏览的页面替换成广告页面,因为这年头直接执行
window.open()
弹广告多半会被拦截掉。虽然使用上面提到的
sandbox
属性可以阻止这一劫持行为,但很少有网站会加这个属性,于是 Chrome 就有所行动了。
第一次尝试:屏蔽那些跨域 iframe 中非用户行为触发的 top.location = 行为
如果是用户主动触发的,比如用户主动点击了淘宝登录框里的登录按钮,那没毛病,允许跳转。其它的,非用户触发的,很有可能就是恶意广告了。
Chrome 56 实现了这一改动,结果 facebook、Microsoft、eBay 等很多网站的页面挂了,其中大多是登录框。就是在这个时间段,淘宝登录框将之前用的
top.location =
方式改成了
postMessage
的方式,这也是 Chrome 推荐的修复方式。
Chrome 见状便回滚了这一改动,准备调研修复后再在 57 中重新发布。
第二次尝试:页面跳转时保留“用户行为触发标识”
56 中的改动哪里出问题了?为什么造成了那么多 “break the web” 的表现?经过分析,是因为很多 ifame 在接受用户点击后,都会发生一次页面跳转(比如 POST 账号密码到后台),然后在跳转后的那个页面里才会执行
top.location =
,用户显然不会点击跳转之后的页面。比如用户在登陆淘宝时,输入用户名、密码,点击登录按钮,注意点击了的这个页面会立刻消失,跳转后的页面用户没有点击过,自然就被拦截了。
于是 57 改成了,只要这个 iframe 被用户点击过,即便发生了页面二次、三次跳转,也一路保留这个标识,这样上面说的淘宝登录框的操作就不会被拦截了,但万万没想到,还是有人报页面挂了。
其中有一个场景是大家都用过的,在国内很流行的场景,扫码登录。腾讯的人报 bug 说,微信的登录框中使用扫码登录,既然是扫码登录,就没有任何需要用户点击的登录按钮了,手机扫码完,页面自动跳转,和恶意广告的行为无法区分。
于是 57 里的改动又回滚了,Chrome 的人觉的需要用户参与进来了,只有用户知道这次跳转行为是不是恶意的、是不是他想要的。
第三次尝试:让用户选择是否继续拦截
然后这一等就是一年多,我从未婚等到了已婚,迎来了 Chrome 68。在 Chrome 68 里,拦截规则和 57 里一样,但新接入了已经使用多年的弹窗拦截功能,复用了之前的拦截提示、允许本次页面跳转、添加永久例外等功能。下面用一个真实案例 https://www. weiyun.com/ 演示一下:
https://www.zhihu.com/video/995226330631442432之前的弹窗拦截界面上新增了一种叫“重定向”的类型,指代这种非弹窗类型的恶意跳转。
Chrome 68 目前还在 beta 通道,微信的同行们要是看到了可以修复一下,其它在自己产品中用到了跨域
href
跳页的同学,也装个测试版测一下,看看有没有问题。