一个全透明的 iframe 盖在目标页面上,用户以为点的是「领取奖品」,其实是银行设置页里的按钮。
Demo 里那行 iframe 一共 135 字节。
防御不在业务代码里,在响应首部:X-Frame-Options 或 CSP 的 frame-ancestors。
上一节是「替你发请求」,这一节是「骗你亲自点」。用户的每一次点击都是真的。
一、透明的那一层
攻击页面做两件事:用一个 opacity: 0 的 iframe 把目标站点整个铺满,再在自己这一层放上诱饵按钮,
位置正好压在 iframe 里那个真按钮上。
用户看到的是诱饵,点下去落在 iframe 里。浏览器不会告诉他这一下点到了另一个文档。
Demo 里那行 iframe 一共 135 字节——这一招的全部成本就这么点。
和 XSS 的区别值得记牢:XSS 要往页面里塞脚本,得先找到注入点;
点击劫持一行脚本都不需要,目标站点也不必有任何漏洞。
这一条特别值得记住:它不需要用户做错任何事,也不需要服务端有漏洞。
服务端一切正常,用户也确实点了他看到的按钮——只是他心里想的是另一个按钮。
二、为什么修法在首部里
被利用的不是某个业务逻辑,而是「浏览器允许一个页面把另一个页面嵌进来」这件事。
所以要关的也是这个开关。
X-Frame-Options: DENY 完全不许被嵌;SAMEORIGIN 只许同源嵌。
它是老方案,兼容性极好,但只能表达这两三档,列不出具体的源。
CSP 的 frame-ancestors 更细,可以列一串允许的源,none 等于 DENY。
两个都发时,支持 CSP 的浏览器按 CSP 判。
一个坑:frame-ancestors 只在响应首部里有效,写进 HTML 的 <meta> 不管用。
另一个坑:X-Frame-Options 只防「被嵌」,不防 XSS,两件事要分别做。
三、实测四种设置
Demo 对一个页面分别用四种设置发响应,看响应首部里有没有那一行、值是什么。
结果:什么都不设的时候一条相关首部都没有;
DENY、CSP 的 frame-ancestors、SAMEORIGIN 三种都发出去了,值也对。
这里必须说清楚:最后一列「能不能被套」是按规范推的判定,不是渲染结果。
本机没有浏览器,我没法真的把一个页面套进另一个页面看它拦不拦。
能确定的是两件事:响应首部确实发出去了(Demo 打出来的那几行就是);
而浏览器按规范遇到这两个首部就会拒绝渲染。这两条加起来,结论才站得住。
四、动手 Demo:四种设置,只有一种漏着
本机起一个服务端,用四种不同的首部设置返回同一个页面,逐行看响应里到底带了什么首部。 再算一下攻击页面里那行 iframe 有多长。
SETTINGS = {
u'none': [],
u'DENY': [(u"X-Frame-Options", u"DENY")],
u'CSP': [(u"Content-Security-Policy", u"frame-ancestors 'none'")],
u'SAME': [(u"X-Frame-Options", u"SAMEORIGIN")],
}
IFRAME = (u'<iframe src="https://bank.example/settings" '
u'style="opacity:0;position:absolute;top:0;left:0;'
u'width:100%;height:100%;z-index:2"></iframe>')
实跑输出:
--------------------------------------------------------------------
点击劫持:一个透明层盖在按钮上
--------------------------------------------------------------------
攻击者的做法:在自己的页面里用一个全透明 iframe 盖住目标站点,
再在同一个位置放一个「领取奖品」按钮。用户以为点的是奖品,其实点的是 iframe 里的按钮。
攻击页面的关键那一行(135 字节):
<iframe src="https://bank.example/settings" style="opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2"></iframe>
服务端设置 响应首部 能不能被套
什么都不设 (没有相关首部) 能
X-Frame-Options x-frame-options: DENY 不能
CSP frame-ancestors content-security-policy: frame-ancestors 'no 不能
X-Frame-Options 只许同源 x-frame-options: SAMEORIGIN 别站不能
注意最后一列是**按规范推的判定**,不是真实渲染结果——本机没有浏览器,
我没法把一个页面真套进另一个页面里看它拦不拦。
能确定的是响应首部发出来了,而浏览器遇到这两个首部就会拒绝渲染。
这一条特别值得记住:它不需要用户做错任何事,也不需要服务端有漏洞。
输出那张四行表里,只有第一行是「能」——什么都不设时,浏览器允许被嵌。 后三行是两种首部的三种写法,判定分别是「不能」「不能」「别站不能」。 每行打出来的首部名和值都跟设置一致。 最后一列是按规范推的判定,不是渲染结果。 本机没有浏览器,所以「拦不拦」我只能给推断,给不了实测——这一点我不想含糊过去。 那行 iframe 量出来 135 字节。 攻击者的整个页面加起来也不过几百字节,不需要脚本、不需要服务器、不需要知道目标站有什么漏洞。 这就是它为什么被列进「不需要漏洞的漏洞」。
30 秒实验
- 打开开发者工具,看一个银行或邮箱站点的响应首部里有没有
X-Frame-Options。 - 本地写两个 HTML,一个用 iframe 嵌另一个,看显示出来是什么。
- 想一想:为什么「跳转前给一个确认页」对点击劫持没用?
常见误区
- 「用户点的是他自己看到的按钮」——他看到的是诱饵,点的是下面那一层。
- 「我们站点没有 XSS,所以不怕」——这一招不需要站点有漏洞。
- 「在 HTML 里写个 meta 就够了」——
frame-ancestors只认响应首部。
自测三问(点开对答案)
1. 点击劫持是怎么实现的? —— 用全透明 iframe 把目标页面铺满,再在同一个位置压一个诱饵按钮。 2. 为什么它不需要目标站点有漏洞? —— 被利用的是「浏览器允许嵌入」这件事,业务代码一切正常。 3. 防它的两个首部是什么? —— `X-Frame-Options` 和 CSP 的 `frame-ancestors`。前两种都还在「人」这一侧。下一种不骗任何人,直接把服务拖到不能响应。
- ☐ (本节完,下一节 11.5.3 DoS攻击)