2018年8月,PortSwigger安全研究员James Kettle在Black Hat USA上展示了一项研究成果。他通过一个精心构造的HTTP请求,成功控制了Firefox浏览器的更新系统——理论上可以影响数千万用户。攻击方式简单到令人不安:发送一个带有恶意X-Forwarded-Host头的请求,缓存在此基础上存储了一个指向攻击者服务器的响应,随后所有请求更新的Firefox用户都会被重定向到攻击者控制的服务器。

这不是一个孤立事件。同一场演讲中,Kettle展示了如何通过缓存投毒接管Red Hat官网、Unity网站、以及多个美国政府网站。当时,很多人认为这是一次高深的技术演示。六年后的2024年,清华大学的研究团队在ACM CCS会议上发表的论文显示:在Tranco Top 1000域名及其子域名中,超过17%的网站仍然存在缓存投毒漏洞,影响的网站数量超过1000个。

Web缓存投毒(Web Cache Poisoning)之所以危险,在于它不是攻击的终点,而是放大器。一个原本"无法利用"的反射型XSS,一旦与缓存投毒结合,就变成了一个影响所有访问者的存储型XSS。一个简单的404错误页面,一旦被缓存,就能造成持续数小时甚至数天的拒绝服务攻击。

缓存键:信任的边界在哪里

要理解缓存投毒,必须先理解缓存是如何决定"两个请求是否相同"的。

当缓存服务器收到一个HTTP请求时,它不会存储整个请求。相反,它提取请求中的某些部分作为"缓存键"(Cache Key),用来标识这个资源。RFC 9111规定,缓存键至少包含请求方法和目标URI。但在实际实现中,大多数缓存服务器只缓存GET请求,因此缓存键通常就是完整的URL。

问题在于,HTTP请求中还有大量内容不会被纳入缓存键——这些被称为"unkeyed input"(非键控输入)。典型的例子包括X-Forwarded-HostX-Forwarded-SchemeX-Original-URL等HTTP头部,以及某些查询参数。

当Web服务器处理请求时,可能会使用这些非键控输入来生成响应内容。例如,一个PHP应用可能会这样生成页面中的绝对URL:

$base_url = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];
echo "<script src='https://{$base_url}/static/app.js'></script>";

这段代码的本意是在反向代理后正确识别主机名。但攻击者可以发送如下请求:

GET /index.php HTTP/1.1
Host: example.com
X-Forwarded-Host: attacker.com

如果X-Forwarded-Host不在缓存键中,缓存服务器会认为这是一个对example.com/index.php的正常请求,并将包含恶意脚本引用的响应存储起来。随后,任何访问example.com/index.php的用户都会收到这个被投毒的响应。

从理论到实战:被改变的安全认知

缓存投毒的概念在2000年代就已经存在,但长期被认为是一个理论威胁——需要太多条件同时满足,实战中几乎不可能成功。Kettle的研究彻底改变了这种认知。

GitHub的$10,000漏洞

Kettle发现GitHub的缓存配置存在一个问题:Varnish缓存服务器默认不将GET请求的请求体(body)纳入缓存键,而后端的Rails框架却会解析这个请求体。这就是著名的"Fat GET"攻击。

攻击者发送如下请求:

GET /users/albinowax HTTP/1.1
Host: github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

report=innocent-victim

由于请求体不在缓存键中,缓存服务器认为这是对/users/albinowash的标准请求。但Rails解析了请求体,将举报目标从原本的用户改为了攻击者指定的innocent-victim。任何访问者看到这个页面并点击举报按钮,都会举报错误的目标。这个漏洞获得了$10,000的赏金。

Mozilla Firefox的更新劫持

Firefox浏览器使用normandy.cdn.mozilla.net来获取"配方"(recipes)——用于控制浏览器行为和安装扩展的指令。Kettle发现这个端点的响应直接使用了X-Forwarded-Host头部来构建JSON响应中的URL:

{
  "action-list": "https://attacker.com/api/v1/action/",
  "recipe-list": "https://attacker.com/api/v1/recipe/",
  ...
}

一旦这个响应被缓存,所有Firefox浏览器在获取配方时都会被重定向到攻击者服务器。虽然Firefox对配方有签名验证,攻击者无法直接安装恶意扩展,但这仍然是一个灾难性的漏洞——攻击者可以引导数千万用户访问任意服务器,或者回放旧版本的配方(包含已知漏洞的扩展)。

Unity官网的路径覆写

Unity官网的Drupal站点支持X-Original-URL头部,这个头部可以覆写请求的路径:

GET /education HTTP/1.1
Host: unity.com
X-Original-URL: /gambling

这个请求会返回/gambling的内容,但缓存键仍然是/education。结果是:所有访问Unity教育页面的用户都会看到赌博网站的内容。

缓存纠缠:攻击技术的进化

2020年,Kettle发表了后续研究"Web Cache Entanglement",展示了更高级的攻击技术。

缓存键注入

Akamai CDN会将缓存键的各个组成部分拼接成一个字符串。但如果攻击者能够在Host头部注入分隔符,就能让两个完全不同的请求产生相同的缓存键:

GET /path1 HTTP/1.1
Host: example.com/path2

如果Akamai简单地使用Host + Path作为缓存键,那么这个请求和GET /path2 HTTP/1.1请求可能有相同的键。这意味着攻击者可以用自己的请求覆盖其他路径的缓存内容。

缓存参数隐藏

许多CDN允许配置"排除某些查询参数",以避免不必要的缓存分片。例如,排除utm_source等分析参数。但如果解析逻辑存在漏洞,攻击者可以"隐藏"任意参数:

# 正常的排除逻辑(Varnish正则)
?utm_source=xxx -> 移除utm_source参数

# 攻击方式
?q=normal&utm_source=x&q=malicious
-> 只移除utm_source,但q参数出现了两次
-> 后端可能取第二个q值(malicious),而缓存键只看第一个q值(normal)

Ruby on Rails框架将分号;也视为参数分隔符,这进一步扩大了攻击面:

?callback=normal;utm_source=x;callback=malicious
-> 缓存键只看第一个callback
-> 后端可能取最后一个callback(malicious)

互联网规模的威胁评估

2024年,清华大学团队开发了HCache系统,对缓存投毒漏洞进行了首次大规模系统性测量。研究结果令人担忧。

测量规模与发现

  • 测试范围:Tranco Top 1000域名及其子域名,共22,114个可缓存域名,51,596个URL
  • 漏洞发现:1,354个存在缓存投毒漏洞的网站,涉及172个顶级域名(17%的测试域名)
  • 新攻击向量:发现7种此前未被公开的攻击类型

新发现的攻击类型

攻击类型 影响网站数 示例头部
内部路由头部攻击 237 Fastly-Client-Ip, X-Amz-Request-Id
认证头部攻击 118 Authorization, X-Auth-User
If条件头部攻击 79 If-Match, If-Range
协议头部攻击 69 X-Forwarded-SSL, X-Forwarded-Proto
Range头部攻击 46 Range: bytes=100-90
Upgrade头部攻击 25 Upgrade: HTTP/0.9
编码头部攻击 19 Accept-Encoding: invalid

受影响的知名网站

研究团队负责任地披露了发现的漏洞,获得了以下公司的确认和致谢:

  • Microsoft - 确认并修复
  • Alibaba (Taobao) - 中危评级,$100赏金
  • Adobe - 评估中,计划修复
  • Huawei - 中危评级,$200赏金
  • SAP - 已发布修复
  • Starbucks, NetEase, Yelp 等15+家公司确认

HTTP/2的持续风险

研究还发现,HTTP/1.1中存在的缓存投毒漏洞在HTTP/2中同样存在。约90%的网站会在HTTP/1.1和HTTP/2之间共享缓存——这意味着攻击者可以通过HTTP/1.1投毒,然后影响HTTP/2用户,反之亦然。

防御:从开发者到运维的全面策略

缓存投毒不是一个单一系统的漏洞,而是缓存服务器与Web服务器之间处理差异的结果。防御需要多层协作。

对于应用开发者

1. 不要信任非键控输入

任何可能被缓存的端点,都不应该使用X-Forwarded-*系列头部来生成响应内容,除非你完全控制缓存配置并确保这些头部被纳入缓存键。

# 危险的做法
host = request.headers.get('X-Forwarded-Host', request.host)

# 安全的做法
host = request.host  # Host头部默认是缓存键的一部分

2. 避免动态生成静态资源

CSS、JS文件通常被认为是静态的,会被长期缓存。但如果这些文件包含动态内容(如基于查询参数生成的代码),就成为了缓存投毒的理想目标。

GET /app.js?callback=alert(1)

如果服务器将callback参数的值直接输出到JS文件中,攻击者就可以注入任意代码。

3. 设置适当的缓存控制头

对于包含用户特定内容或动态生成的响应,使用:

Cache-Control: private, no-cache
Vary: X-Custom-Header  # 如果必须使用特定头部

对于缓存/CDN运维人员

1. 谨慎配置缓存键排除

Cloudflare的文档明确建议:不要排除任何可能影响响应内容的查询参数。如果必须排除(如分析参数),确保应用层面完全不使用这些参数。

2. 将关键头部纳入缓存键

Cloudflare在2018年后默认将X-Forwarded-Host等头部纳入缓存键(当其值与Host不同时)。其他CDN也提供了类似配置选项:

# Nginx示例
proxy_cache_key "$scheme$host$request_uri$http_x_forwarded_host";

3. 禁用错误响应缓存

RFC 9111规定某些错误状态码(如404、410)可以被缓存,但实际配置中应该谨慎:

proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;  # 短时间或不缓存
proxy_cache_valid 500 502 503 504 0;  # 不缓存服务器错误

4. 禁用Fat GET处理

确保缓存服务器不会转发带有请求体的GET请求,或者将请求体纳入缓存键:

# Varnish默认会移除GET请求体
# 确保不要覆盖这个默认行为

架构层面的考虑

1. 静态资源与动态资源分离

将静态资源(CSS、JS、图片)部署到单独的域名或路径,由独立的缓存层处理。动态内容完全禁用缓存,或使用严格的内容协商。

2. 缓存预热与监控

对于关键页面,实施缓存预热,确保第一个请求来自可信源。同时监控缓存命中率异常——突然的高命中率可能意味着缓存被投毒。

3. 缓存分区

对于多租户系统,使用Vary头部或不同的缓存键策略,确保不同租户的内容不会互相污染。

一个持续的挑战

2022年发布的RFC 9111在安全章节中明确提到了缓存投毒:

“Storing malicious content in a cache can extend the reach of an attacker to affect multiple users. Such ‘cache poisoning’ attacks happen when an attacker uses implementation flaws, elevated privilege, or other techniques to insert a response into a cache.”

但标准的更新并不意味着问题的解决。RFC只是给出了原则性指导,实际实现中的差异才是漏洞的根源。

缓存投毒之所以难以根除,是因为它处于多个系统的边界——应用服务器、反向代理、CDN、负载均衡器,每个组件都有自己的缓存逻辑和配置选项。开发者可能不了解缓存配置,运维人员可能不了解应用逻辑,而安全团队可能两者都不熟悉。

Kettle在2018年的演讲标题是"Redefining Unexploitable"(重新定义"无法利用")。六年过去了,这个标题依然准确。那些被认为"无法利用"的反射型XSS、那些被认为是"静态"的资源、那些被标记为"无害"的HTTP头部——在缓存投毒的放大镜下,都可能成为灾难性的漏洞。

当你在设计Web应用时,问问自己:如果这个响应被缓存并分发给数百万用户,会怎样?这个简单的问题,可能避免下一场安全危机。


参考文献

  1. James Kettle. “Practical Web Cache Poisoning: Redefining ‘Unexploitable’.” PortSwigger Research, August 2018.

  2. James Kettle. “Web Cache Entanglement: Novel Pathways to Poisoning.” PortSwigger Research, August 2020.

  3. Yuejia Liang, Jianjun Chen, et al. “Internet’s Invisible Enemy: Detecting and Measuring Web Cache Poisoning in the Wild.” ACM CCS 2024.

  4. RFC 9111: HTTP Caching. IETF, June 2022.

  5. Cloudflare. “How Cloudflare protects customers from cache poisoning.” Cloudflare Blog, August 2018.

  6. Cloudflare. “Avoid Web Cache Poisoning.” Cloudflare Documentation, August 2024.

  7. PortSwigger Web Security Academy. “Web cache poisoning.” https://portswigger.net/web-security/web-cache-poisoning

  8. Nguyen, Lo Iacono, Federrath. “Your Cache Has Fallen: Cache-Poisoned Denial-of-Service Attack.” ACM CCS 2019.