适用场景
网站接入 CDN(Cloudflare、阿里云 CDN、腾讯云 CDN 或自建 Nginx 反向代理缓存)后,攻击者可能利用缓存投毒(Web Cache Poisoning)把恶意内容注入 CDN 缓存,让所有访问者被投毒页面或恶意脚本,危害面被放大到全网。本方案适用于所有使用缓存加速的 Web 站点,重点解决「缓存键(Cache Key)覆盖不安全请求参数/请求头」导致的投毒问题,兼顾自建 Nginx 缓存与商业 CDN 两种场景。
前置条件
- 已接入 CDN 的域名,或使用 Nginx
proxy_cache的自建缓存环境 - 能修改源站 Nginx 配置(本文以 Nginx 1.20+ 为例)
- 了解 HTTP 请求头(Host、X-Forwarded-*)与缓存基本原理
原理说明
CDN 缓存的核心是「缓存键」:默认通常由 Host + URL 路径 组成,响应按此键存储与回源。缓存投毒的两条典型路径:
- 参数型投毒:CDN 把未纳入缓存键的查询参数(如
?debug=1)也拼进回源请求,源站据此返回不同内容(如调试页、报错页),但 CDN 仍按「无参数 URL」存储响应,导致正常访问者命中恶意/异常缓存。 - 请求头投毒:CDN 转发
X-Forwarded-Host、X-Forwarded-Proto等头,源站基于这些头拼接 URL 或切换页面逻辑(如生成绝对链接、跳转),攻击者篡改请求头后,恶意响应被缓存供所有人共享。
防御思路:把缓存键收紧到只含可信变量,并让源站忽略/校验不可信请求头。
操作步骤
1. 收紧 Nginx 缓存键(自建缓存场景)
# /etc/nginx/conf.d/cache.conf
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=wafai_cache:10m max_size=10g inactive=60m use_temp_path=off;
# 站点 server 块内
server {
listen 443 ssl;
server_name www.wafai.cn;
location / {
proxy_cache wafai_cache;
# 缓存键只保留 请求方法 + Host + 规范化 URI,排除全部查询参数与额外头
proxy_cache_key "$scheme$host$uri";
# 限制缓存方法
proxy_cache_methods GET HEAD;
# 仅缓存部分扩展名
proxy_cache_valid 200 301 302 10m;
proxy_set_header Host $host;
# 不转发不可信头:清空 X-Forwarded-Host,防止被用于拼接 URL
proxy_set_header X-Forwarded-Host $host;
proxy_pass http://127.0.0.1:8080;
}
}
关键点:$uri 是 Nginx 规范化后的路径(不含原始查询串),配合不缓存参数,可避免「参数不同但共享同一缓存键」;对确需按参数分缓存的接口,改用白名单方式显式列出:
# 只把白名单参数计入缓存键
proxy_cache_key "$scheme$host$uri?$arg_page";
2. 校验/清洗转发头(源站视角)
源站应只信任来自 CDN 边缘节点或内网代理的转发头。以 Nginx 为例,用 map 限制 X-Forwarded-Host 取值:
# 仅当来源为可信代理网段时保留,否则重置为 Host
map $remote_addr $safe_fwd_host {
default $host;
192.168.1.0/24 $http_x_forwarded_host;
}
location / {
proxy_set_header X-Forwarded-Host $safe_fwd_host;
}
3. 限制可缓存响应(商业 CDN 场景)
在 Cloudflare Cache Rules / 阿里云 CDN 缓存配置中:
- 缓存键(Cache Key)只保留
scheme + host + path,关闭「忽略查询字符串」之外的额外键变量; - 启用「忽略查询字符串」时,务必同时确认源站不会因参数返回差异内容;
- 对
Set-Cookie、私有接口、带Authorization的响应禁止缓存; - 配置 WAF 规则拦截可疑的
X-Forwarded-Host修改与畸形 Host 头。
4. 增加缓存命中校验响应头
# 源站 Nginx 输出安全响应头,辅助 CDN 决策
add_header X-Content-Type-Options "nosniff" always;
add_header Cache-Control "public, max-age=600" always;
# 若响应因人而异,明确用 Vary 声明,避免 CDN 误缓存
# add_header Vary "Accept-Encoding, X-Custom" always;
配置验证
# 1. 语法检查并重载
sudo nginx -t && sudo systemctl reload nginx
# 2. 验证参数不污染缓存键:两次请求应返回相同内容、命中同一缓存
curl -sI "https://www.wafai.cn/?debug=1" | grep -iE "x-cache|cache-control"
curl -sI "https://www.wafai.cn/" | grep -iE "x-cache|cache-control"
# 3. 验证 X-Forwarded-Host 投毒无效:篡改后仍返回正常页面
curl -s -H "X-Forwarded-Host: evil.example.com" "https://www.wafai.cn/" | head -c 300
# 期望输出为正常首页内容,不含 evil.example.com 相关链接
# 4. 检查源站访问日志,确认带参数请求并未触发差异化响应
grep "debug=1" /var/log/nginx/access.log | tail -n 5
常见问题
FAQ 1:为什么只设置 proxy_cache_key 还不够?
缓存键只是「存哪里」的问题;如果源站对 ?debug=1 返回了不同内容,CDN 仍会把差异响应写入同一个键。因此必须双管齐下:① 缓存键排除参数/可信头;② 源站对参数、转发头做归一化处理(忽略无关参数、重置 X-Forwarded-Host),保证同一缓存键下内容一致。两者缺一不可。
FAQ 2:投毒已经发生,如何应急清理?
立即执行:① 在 CDN 控制台全量刷新/清除缓存(按 URL 或目录);② 源站检查并修复生成差异化响应的接口(参数、头解析逻辑);③ 应用上述缓存键与转发头加固配置;④ 复查 Vary/Cache-Control 头,确认敏感响应未被缓存;⑤ 保留攻击前后日志(含 X-Forwarded-Host、referer),用于溯源和确认清除范围。
总结
本文从缓存键与转发头两条投毒路径出发,给出了自建 Nginx 与商业 CDN 场景下的防御配置:缓存键收紧为 scheme+host+uri、白名单参数、可信转发头校验、敏感响应不缓存,并提供了完整的验证命令。缓存投毒的核心在于「CDN 看到的键」与「源站返回的内容」的一致性,配置时需同时约束两端。建议接入 CDN 的站点将本方案纳入上线检查清单,并定期用上述 curl 用例回归验证。