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/ 演示一下:
之前的弹窗拦截界面上新增了一种叫 "重定向" 的类型, 指代这种非弹窗类型的恶意跳转.
Chrome 68 目前还在 beta 通道, 微信的同行们要是看到了可以修复一下, 其它在自己产品中用到了跨域 href 跳页的同学, 也装个测试版测一下, 看看有没有问题.
来源: https://juejin.im/entry/5b33121e6fb9a00e9402b85f