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

X-XSS-Protection 是当年让浏览器开启「XSS 过滤器」的开关。 如今 Chrome、Edge 已移除该功能,Firefox 从没实现过——这个首部发出去基本等于白发。

📝 本节和前面几节的关系

上一节防点击劫持,这一节防 XSS。两个字段放一起看很有意思:一个还有效,一个已经被主流实现抛弃。

一、三种取值

0 是关掉过滤器,1 是开着并在发现可疑时改页面,1; mode=block 是开着并在发现可疑时整页拦掉。 不发就是不表态。 Demo 里四种情况:111、132、132、144 字节。0 和 1 一样长,只有带 mode=block 的多 12 字节。 注意 1 那个「改页面」的做法:它会试图把可疑的脚本片段从响应里剔除。 这种「猜哪一段像攻击」的思路本身就很脆——攻击者可以构造出让过滤器误判的输入,把正常内容改坏, 甚至绕过。

二、为什么它退场了

Chrome 从 78 版起移除 XSS Auditor,Edge 跟着做。 理由是这套机制误报多、可绕过,而且它带来的虚假安全感比它能防的漏洞更糟。 Firefox 从头到尾就没实现过这一套,它走的是另一条路(CSP)。所以今天主流浏览器全部忽略这个首部。 于是出现一个反直觉的建议:现在很多安全扫描会要求把它显式写成 0。 不是因为它有用,而是为了关掉老浏览器里那个会帮倒忙的过滤器。

三、真正管用的是 CSP

CSP(Content-Security-Policy)管的是「哪些脚本可以跑」, 比如 script-src 'self' 就只允许同源脚本。 差别在思路上:X-XSS-Protection 是「猜页面里有没有像攻击的字符串」,CSP 是「列出允许的, 其余一律不许」。前者是黑名单加猜测,后者是白名单。 白名单当然也有代价——配错了会把正常功能挡掉,而且上线后要盯着报告(report-uri)慢慢收。 但这是工程问题,不是机制缺陷。

四、动手 Demo:四种取值,只差几十字节

同一个页面分别不发、写 0、写 1、写 1; mode=block,量出整条响应的大小,顺带看清每个值的语义。

cases = [(None, u'(不发)'),
         (u'0', u'关掉过滤器'),
         (u'1', u'发现可疑就改页面'),
         (u'1; mode=block', u'发现可疑就整页拦掉')]

def make(val):
    def fn(raw, st):
        hd = [(u'Content-Type', u'text/html; charset=utf-8')]
        if val is not None:
            hd.insert(0, (u'X-XSS-Protection', val))
        return X.reply(u'HTTP/1.1 200 OK', hd, PAGE)
    return fn

实跑输出:

--------------------------------------------------------------------
X-XSS-Protection:一个已经退场的开关
--------------------------------------------------------------------
  X-XSS-Protection     整条响应   含义
  (不发)             111        不表态
  0                    132        关掉过滤器
  1                    132        开着,发现可疑就改页面
  1; mode=block        144        开着,发现可疑就整页拦掉

  这四行的差别只在几十字节,但它描述的那个机制已经不存在了:
  Chrome 从 78 版起移除 XSS Auditor,Edge 跟着做;
  Firefox 从来没实现过这一套。也就是说今天主流浏览器全部忽略它。

  更麻烦的是 `1`(不带 mode)这个值:老 Chrome 的「改页面」做法
  本身会引入新的绕过手法,反而增加攻击面。所以现在很多安全扫描
  会建议**显式写成 `0`**,把它彻底关掉,免得老浏览器帮倒忙。

  真正管用的是 CSP:`Content-Security-Policy: script-src 'self'`。
  它管的是「哪些脚本能跑」,而不是「页面里有没有像攻击的字符串」——
  前者是白名单,后者是猜,差别就在这里。

四行响应 111、132、132、144 字节。0 和 1 都是 132,只有 1; mode=block 那行多 12 字节。 字节上差得不多,但语义差得远:0 是「别管」,1 是「管,但要改我的页面」, 1; mode=block 是「管,宁可整页不显示」。 本机没有浏览器,所以这四行只能验证「首部发出去了」, 验证不了「浏览器到底做了什么」——因为主流浏览器现在什么都不做。 这也是这一节的核心:一个首部格式正确、语义清晰,仍然可能已经没有实现。 看规范之前,先确认还有谁在实现它。

30 秒实验

  1. 查一下自己常用浏览器当前版本还认不认这个首部。
  2. 写一条 Content-Security-Policy: script-src 'self',比较两者思路的差别。
  3. 想一想:为什么「猜哪段像攻击」这种思路天生容易误报?

常见误区

  1. 「它还在保护我的站点」——主流浏览器已移除该机制。
  2. 「1 比 0 安全」——1 的改写行为本身会引入新问题。
  3. 「有它就够了,不用 CSP」——方向错了,CSP 才是现在的手段。
自测三问(点开对答案) 1. `1; mode=block` 和 `1` 的区别是什么? —— 前者发现可疑就整页拦掉,后者只是试图改写页面。 2. 为什么现在建议显式写 `0`? —— 为了关掉老浏览器里那个会误判、可能帮倒忙的过滤器。 3. 替代它的是什么机制? —— CSP,走白名单路线,管「哪些脚本能跑」。

两个防攻击的字段讲完,剩下两个更尴尬:一个是「请求对方别跟踪」,一个是「声明自己的隐私承诺」。

相关笔记