X-Frame-Options 决定这个页面能不能被别的页面套进 iframe。
取值只有 DENY 和 SAMEORIGIN 靠得住,ALLOW-FROM 早被主流浏览器放弃。
Cookie 两节讲的是「记住你是谁」。最后四个字段是「附加约定」,这一节防的是点击劫持。
一、它防的是点击劫持
攻击手法很朴素:把银行页面套进一个透明 iframe,上面盖一个「领取奖品」的按钮。
用户以为点的是奖品,实际点的是「转账确认」。
X-Frame-Options: DENY 就是「谁都不许套我」,SAMEORIGIN 是「只有同源的能套」。
Demo 里两种响应分别是 134 和 140 字节,不发的那个是 111 字节。
注意这是响应首部,只能由被套的那一方声明。
想套别人的页面,靠这个字段是做不到的——只能靠对方配合。
二、ALLOW-FROM 为什么死了
它本来想表达「只允许某个站点套」,但实现起来很别扭:浏览器在渲染 iframe 的那一刻,
未必拿得到父页面的完整来源,判断链条容易断。
结果是 Chrome、Safari 都没实现,Firefox 后来也移除了。
Demo 里那行 ALLOW-FROM https://a.example 发出去是 158 字节,格式完全合法,
但今天的浏览器会整行忽略。
替代方案是 CSP 的 frame-ancestors:
Content-Security-Policy: frame-ancestors 'self' https://a.example。
它支持多个来源,而且是标准的一部分。
三、能测的和测不到的
本机能测的只有「首部发出去占多少字节」——111 到 158 不等。渲染行为要真浏览器,这个沙箱里没有。
所以 Demo 里那张表的最后一列(页面被 evil.example 套时会怎样)是按规范推的判定,
不是真实渲染结果。这一点得说清楚,不能拿推算当实测。
顺带一个细节:值大小写不敏感,Demo 里故意写小写 deny,解析结果和 DENY 一样(都是 134 字节)。
但老实现有按字面比较的,实践上仍建议写大写。
四、动手 Demo:五种取值,量字节、推判定
同一个页面配五种 X-Frame-Options(不发、DENY、SAMEORIGIN、ALLOW-FROM、小写 deny),
量出整条响应的大小,再按规范推算「被跨源页面套住时会不会被允许」。
def judge(val, page_origin, frame_origin):
if not val:
return u'不限制'
v = val.strip().upper()
if v == u'DENY':
return u'拒绝(任何来源都不行)'
if v == u'SAMEORIGIN':
return u'允许(同源)' if page_origin == frame_origin else u'拒绝(跨源)'
if v.startswith(u'ALLOW-FROM'):
return u'只许指定站点(已废弃)'
return u'值非法,多数浏览器按 DENY 处理'
实跑输出:
--------------------------------------------------------------------
X-Frame-Options:许不许被套进 iframe
--------------------------------------------------------------------
X-Frame-Options 整条响应 页面在 evil.example 里被套时
(不发) 111 不限制
DENY 134 拒绝(任何来源都不行)
SAMEORIGIN 140 拒绝(跨源)
ALLOW-FROM https://a.example 158 只许 https://a.example(已废弃)
deny 134 拒绝(任何来源都不行)
本机没有浏览器,所以上面最后一列是**按规范推的判定**,不是真看渲染结果。
能实测的只有一件事:这一行发出去占多少字节——从 111 到 158 不等。
三个取值的实际状况:
DENY 最狠,谁都不许套。适合后台、支付页。
SAMEORIGIN 只许同源套。适合需要自己的站点里嵌自己的页面。
ALLOW-FROM 只许指定站点。**主流浏览器早就不支持了**,别再用。
最后一行故意写小写 `deny`:规范要求值大小写不敏感,
但老实现有按字面比较的,所以实践上仍建议写大写。
要表达「只许某些源」,现在的标准做法是 CSP 的 `frame-ancestors`。
五行响应:111、134、140、158、134 字节。不发的那行最短,发的越具体越长。
DENY 和小写 deny 都是 134 字节,判定也一致——说明解析确实做了大小写归一。
ALLOW-FROM 那行 158 字节最长,格式没问题,但今天的浏览器会忽略它。
这就是「写得对照样没用」的典型。
最后一列的判定是推算,不是实测。这一点在正文里也再说一遍,免得把「按规范应该」当成「本机验过」。
30 秒实验
- 把值改成
SAMEORIGIN, DENY(两个并列),看解析会不会失败。 - 查一查
frame-ancestors的写法,想想要不要和这个首部同时发。 - 想一想:为什么「防点击劫持」这件事只能靠被套方自己声明?
常见误区
- 「
ALLOW-FROM还能用」——主流浏览器早已不支持。 - 「这个首部能阻止我去套别人」——它只约束自己被不被套。
- 「值必须大写」——大小写不敏感,但建议大写以兼容老实现。
自测三问(点开对答案)
1. `DENY` 和 `SAMEORIGIN` 的区别是什么? —— 前者谁都不许套,后者只许同源页面套。 2. `ALLOW-FROM` 为什么被放弃? —— 浏览器在渲染时未必拿得到父页面完整来源,判断不可靠;且多数浏览器从未实现。 3. 现在的替代标准是什么? —— CSP 的 `frame-ancestors` 指令。点击劫持防住了,接着是一个防 XSS 的老开关——它已经彻底退场。
- ☐ (本节完,下一节 6.8.2 X-XSS-Protection)