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-Host、X-Forwarded-Scheme、X-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应用时,问问自己:如果这个响应被缓存并分发给数百万用户,会怎样?这个简单的问题,可能避免下一场安全危机。
参考文献
-
James Kettle. “Practical Web Cache Poisoning: Redefining ‘Unexploitable’.” PortSwigger Research, August 2018.
-
James Kettle. “Web Cache Entanglement: Novel Pathways to Poisoning.” PortSwigger Research, August 2020.
-
Yuejia Liang, Jianjun Chen, et al. “Internet’s Invisible Enemy: Detecting and Measuring Web Cache Poisoning in the Wild.” ACM CCS 2024.
-
RFC 9111: HTTP Caching. IETF, June 2022.
-
Cloudflare. “How Cloudflare protects customers from cache poisoning.” Cloudflare Blog, August 2018.
-
Cloudflare. “Avoid Web Cache Poisoning.” Cloudflare Documentation, August 2024.
-
PortSwigger Web Security Academy. “Web cache poisoning.” https://portswigger.net/web-security/web-cache-poisoning
-
Nguyen, Lo Iacono, Federrath. “Your Cache Has Fallen: Cache-Poisoned Denial-of-Service Attack.” ACM CCS 2019.