Chrome 将默认屏蔽“非用户行为触发”的父页面跳转

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 里一样,但新接入了已经使用多年的弹窗拦截功能,复用了之前的拦截提示、允许本次页面跳转、添加永久例外等功能。下面用一个真实案例 weiyun.com/ 演示一下:

https://www.zhihu.com/video/995226330631442432

之前的弹窗拦截界面上新增了一种叫“重定向”的类型,指代这种非弹窗类型的恶意跳转。

Chrome 68 目前还在 beta 通道,微信的同行们要是看到了可以修复一下,其它在自己产品中用到了跨域 href 跳页的同学,也装个测试版测一下,看看有没有问题。

编辑于 2018-06-27 09:27

文章被以下专栏收录