📘 第六章 HTTP首部
📄 一句话概括

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 秒实验

  1. 把值改成 SAMEORIGIN, DENY(两个并列),看解析会不会失败。
  2. 查一查 frame-ancestors 的写法,想想要不要和这个首部同时发。
  3. 想一想:为什么「防点击劫持」这件事只能靠被套方自己声明?

常见误区

  1. 「ALLOW-FROM 还能用」——主流浏览器早已不支持。
  2. 「这个首部能阻止我去套别人」——它只约束自己被不被套。
  3. 「值必须大写」——大小写不敏感,但建议大写以兼容老实现。
自测三问(点开对答案) 1. `DENY` 和 `SAMEORIGIN` 的区别是什么? —— 前者谁都不许套,后者只许同源页面套。 2. `ALLOW-FROM` 为什么被放弃? —— 浏览器在渲染时未必拿得到父页面完整来源,判断不可靠;且多数浏览器从未实现。 3. 现在的替代标准是什么? —— CSP 的 `frame-ancestors` 指令。

点击劫持防住了,接着是一个防 XSS 的老开关——它已经彻底退场。

相关笔记