跳转到帖子

咖啡馆实战:网站接入 CDN 后的真实源站识别、缓存越权与日志留痕排查

精选回复

发布于

背景

网站接入 CDN、反向代理或云加速服务后,访问链路通常变为“客户端 → CDN/WAF → Nginx → 应用”。这类架构能降低源站压力,但也容易引入三类边界问题:源站仍可被公网直接访问、动态响应被错误缓存、应用无条件信任客户端提交的代理请求头。

下面以一次已获授权的网站安全检查为例,说明如何验证这些问题,并给出 Nginx 层面的修复方法。测试前应明确授权域名、源站 IP、允许的请求频率和时间窗口,不对第三方 CDN 节点做超出范围的扫描。

问题定位

目标站点通过 CDN 对外提供服务,但运维反馈应用日志中出现了无法对应 CDN 节点的访问 IP,同时部分登录用户偶尔看到其他账号的页面片段。初步判断需要分别检查源站访问控制、缓存键设计以及真实客户端 IP 的解析方式。

  • 源站是否只允许 CDN 回源地址访问。
  • 登录态、用户中心、订单接口是否进入了共享缓存。
  • Nginx 是否直接信任 X-Forwarded-For 或 X-Real-IP。
  • CDN 日志、Nginx access log 与应用审计日志能否通过请求标识关联。

一、确认请求实际经过 CDN

先对授权域名执行低频请求,记录解析结果、TLS 证书和响应头。Server、Via、Age、X-Cache 等字段只能作为线索,不能单独作为结论,因为这些字段可能被隐藏或伪造。

dig +short  A
curl -skI 
curl -skI --http2 
openssl s_client -connect  -servername  </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -ext subjectAltName

如果已由资产所有者提供源站 IP,可使用 Host 头和 SNI 做定向验证,不需要通过扫描或第三方历史数据库猜测源站。下面的命令只适用于明确授权的源站地址:

ORIGIN_IP=192.0.2.10
curl -skI --resolve  \
  

curl -sk --resolve  \
   -o /dev/null \
  -w 'status=%{http_code} remote=%{remote_ip}\n'

如果通过源站 IP 加 Host/SNI 后能够获得与 CDN 域名一致的业务页面,说明源站存在绕过 CDN 直接访问的可能。此时还应比较安全头、限速策略和错误页,确认直接访问是否同时绕过了 WAF 或鉴权前置层。

二、检查动态页面的缓存边界

缓存越权常见于缓存键未包含必要的身份维度,或者 Nginx/CDN 忽略了响应中的 Set-Cookie、Cache-Control。验证时应使用专用测试账号和无敏感数据的测试记录,禁止读取真实用户内容。

使用两个已授权测试账号分别请求同一路径,重点观察 Age、X-Cache、ETag、Vary 和 Cache-Control。响应正文不要直接 diff 全量敏感内容,可以仅计算哈希并检查测试账号标记。

curl -skD a.headers \
  -H 'Cookie: session=TEST_ACCOUNT_A' \
  ' \
  -o a.body

curl -skD b.headers \
  -H 'Cookie: session=TEST_ACCOUNT_B' \
  ' \
  -o b.body

sha256sum a.body b.body
grep -iE '^(age|x-cache|cache-control|vary|set-cookie|etag):' a.headers b.headers
grep -Eo 'TEST_ACCOUNT_[AB]|marker=[AB]' a.body b.body

若账号 B 的响应命中了账号 A 生成的缓存,或携带 Cookie 的请求仍出现共享缓存命中,应立即停止扩大验证范围,保存请求时间、请求标识和最小化证据。不要通过枚举账号、订单号等方式证明影响。

Nginx 使用 FastCGI 缓存时,可以显式跳过登录态和敏感路径:

map $http_cookie $has_session {
    default 0;
    ~*"(session|auth_token|jwt)=" 1;
}

map $request_uri $sensitive_path {
    default 0;
    ~*^/(login|logout|account|admin|api/private)(/|$) 1;
}

server {
    location / {
        set $skip_cache 0;
        if ($has_session) { set $skip_cache 1; }
        if ($sensitive_path) { set $skip_cache 1; }

        fastcgi_cache SITE_CACHE;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        fastcgi_cache_valid 200 5m;
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Nginx 的 if 使用场景需要谨慎。生产环境也可以通过 map 组合出单一变量,或按敏感路径拆分 location。关键不是照搬配置,而是确保任何带身份状态的响应都不进入共享缓存。

应用层还应对用户数据响应返回明确策略:

Cache-Control: private, no-store
Pragma: no-cache
Vary: Cookie, Authorization

Vary 不能替代 no-store。对于账号、订单、后台和私有 API,优先选择不缓存,而不是把完整 Cookie 或 Authorization 放入缓存键,这会增加缓存碎片和令牌泄露风险。

三、排查真实客户端 IP 信任链

很多应用直接读取 X-Forwarded-For 的第一个值。如果源站可直连,攻击者可以自行构造该请求头,造成审计记录伪造、IP 白名单绕过或限速失效。

先查看 Nginx 的实际生效配置,而不是只检查某一个配置文件:

sudo nginx -T 2>&1 | grep -nE 'set_real_ip_from|real_ip_header|real_ip_recursive|log_format|allow|deny'

curl -skI  \
  -H 'X-Forwarded-For: 127.0.0.1, 198.51.100.20' \
  -H 'X-Real-IP: 127.0.0.1'

测试请求本身不能证明存在绕过,还需由运维在日志中确认 remote_addr、http_x_forwarded_for 和最终解析后的 realip_remote_addr。安全配置应只信任 CDN 官方公布并定期同步的回源网段:

# 示例地址仅用于文档,生产环境替换为 CDN 官方回源网段
set_real_ip_from 203.0.113.0/24;
set_real_ip_from 2001:db8:1234::/48;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# 不要使用以下配置:它会信任任意来源
# set_real_ip_from 0.0.0.0/0;

如果 CDN 使用自定义真实 IP 头,应以供应商文档为准,并在源站网络层限制只有 CDN 回源地址可以连接。仅配置 real_ip 模块不能阻止源站被直连。

四、建立可关联的日志留痕

排障时最有价值的是一条请求在 CDN、Nginx 和应用之间具有同一个请求标识。客户端提交的 Request-ID 不应直接作为可信审计字段,建议由边缘层生成,源站校验格式后继续传递。

log_format security_json escape=json
  '{"time":"$time_iso8601",'
  '"request_id":"$request_id",'
  '"remote_addr":"$remote_addr",'
  '"realip_peer":"$realip_remote_addr",'
  '"xff":"$http_x_forwarded_for",'
  '"host":"$host","method":"$request_method",'
  '"uri":"$uri","args":"$args",'
  '"status":$status,"bytes":$body_bytes_sent,'
  '"request_time":$request_time,'
  '"upstream_time":"$upstream_response_time",'
  '"cache":"$upstream_cache_status",'
  '"ua":"$http_user_agent"}';

access_log /var/log/nginx/security_access.json security_json;
add_header X-Request-ID $request_id always;
proxy_set_header X-Request-ID $request_id;

日志中不应记录 Cookie、Authorization、完整 POST 正文和密码重置令牌。查询参数可能包含令牌或个人信息,条件允许时应在应用层记录规范化路由而不是原始参数。

可以按授权测试时间窗口快速筛选源站直连、伪造代理头和异常缓存命中:

# 检查近期是否存在带 XFF 但连接来源不属于预期代理的记录
jq -r 'select(.xff != "" and .realip_peer != "") |
  [.time,.request_id,.remote_addr,.realip_peer,.xff,.host,.uri,.status,.cache] | @tsv' \
  /var/log/nginx/security_access.json | tail -n 100

# 统计敏感路径上的缓存状态
jq -r 'select(.uri | test("^/(account|admin|api/private)")) | .cache' \
  /var/log/nginx/security_access.json | sort | uniq -c

# 配置检查和安全重载
sudo nginx -t && sudo systemctl reload nginx

五、源站访问控制与权限边界

推荐在云安全组或主机防火墙上限制 80、443 端口的来源,仅允许 CDN 回源网段和独立管理出口。Nginx 的 allow/deny 可以作为第二层控制,但不应代替网络层策略。

server {
    listen 443 ssl;
    server_name 

    # 示例网段,实际配置必须使用 CDN 官方地址清单
    allow 203.0.113.0/24;
    allow 2001:db8:1234::/48;
    deny all;

    location = /health {
        access_log off;
        return 200 'ok\n';
    }
}

还应单独检查默认虚拟主机。若未知 Host 仍被路由到业务站点,直接访问源站 IP 时可能泄露页面或证书信息:

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    return 444;
}

上线前应确认健康检查来源,否则收紧安全组或 deny 规则后可能导致 CDN 将源站判定为不可用。建议先在日志中观察至少一个完整业务周期,再灰度启用阻断。

风险判断

问题直接影响优先级
源站公网直连绕过 CDN、WAF、限速和统一证书策略高
私有响应进入共享缓存跨用户数据泄露或页面内容串号严重
无条件信任代理头日志伪造、IP 白名单或限速边界失效高
缺少统一请求标识事件无法跨层关联,增加取证成本中
日志记录令牌和 Cookie日志系统成为二次泄露源高

修复与复测建议

  • 在安全组和防火墙层限制源站端口,只允许 CDN 回源网段;建立地址清单自动更新和变更审计。
  • 对登录、后台、订单、用户中心和私有 API 设置 private, no-store,并在 CDN 与 Nginx 两层同时验证。
  • 只信任明确的代理网段;保留连接对端地址、代理头原值和解析后地址,避免覆盖取证字段。
  • 默认虚拟主机拒绝未知 Host,业务虚拟主机启用明确的 server_name。
  • 使用两个隔离测试账号复测缓存,不接触真实用户数据;复测后清理测试缓存与测试会话。
  • 修复前后分别保存 nginx -T、响应头、请求标识和最小化日志片段,便于回滚和审计。

总结

网站加速并不是简单地把域名解析到 CDN。真正需要关注的是回源访问边界、缓存身份边界、代理头信任边界和日志关联能力。排查时应从请求链路逐层验证:先确认入口,再验证源站是否可直连,然后检查动态缓存与真实 IP 解析,最后通过统一请求标识闭合证据链。这样既能控制授权测试影响,也能让修复结果具备可验证性。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

这类排查建议分成三条证据链:源站暴露面、缓存隔离、边缘到源站的日志关联。只看页面内容或响应头相似度,容易把同一套默认站点误判成真实源站。

1. 源站识别要以“自有资产核验”为准

先检查现网 DNS、历史解析、证书签发记录和云资产清单,重点找 CDN 接入前遗留的 A/AAAA 记录、测试域名、邮件头或监控平台中暴露的地址:

dig +short example.com A
dig +short example.com AAAA
dig +short  CNAME

# 在确认属于自有资产后,核验候选地址是否承载该虚拟主机
curl -kI --resolve  \
  

openssl s_client -connect 203.0.113.10:443 \
  -servername  </dev/null 2>/dev/null |
  openssl x509 -noout -subject -issuer -ext subjectAltName

证书、标题和静态文件哈希只能作为线索,最终应回到源站访问日志或负载均衡日志确认。更重要的是验证非 CDN 出口能否直连:正确状态应为超时、拒绝连接或明确的 403,而不是仍能拿到业务页面。

源站侧至少做两层限制:安全组只放行 CDN 官方回源网段;回源请求再使用 CDN 重写的专用请求头或 mTLS。不要只依赖可由客户端伪造的 X-Forwarded-For。

# Nginx 示例:网络层白名单仍应放在安全组/防火墙
allow 192.0.2.0/24;       # 替换为 CDN 官方回源网段
allow 2001:db8:100::/48;
deny all;

# 辅助校验,密钥应由 CDN 在回源阶段覆盖,不能透传客户端同名头
if ($http_x_origin_verify != "随机长值") {
    return 403;
}

2. 缓存越权不要只测“是否命中”

建议准备两个权限不同的测试账号,对同一 URL 交叉请求,并记录 Age、Cache-Status、X-Cache、响应体哈希及账号标识。高风险信号是:带认证信息的首个请求产生缓存,第二个账号或匿名请求随后命中相同对象。

# A 账号先请求
curl -skD a.hdr -o a.body \
  -H 'Authorization: Bearer TOKEN_A' \
  '

# 匿名及 B 账号复测
curl -skD anon.hdr -o anon.body \
  '

curl -skD b.hdr -o b.body \
  -H 'Authorization: Bearer TOKEN_B' \
  '

sha256sum a.body anon.body b.body
grep -Ei '^(age|cache-status|x-cache|set-cookie|cache-control|vary):' \
  a.hdr anon.hdr b.hdr

需要同时检查缓存键是否遗漏了主机名、关键查询参数、设备分流头和内容协商头。尤其注意 CDN 是否忽略查询串、是否把扩展名当成静态资源、是否缓存 302,以及源站返回 Set-Cookie 时边缘节点仍强制缓存。

认证接口的原则应是默认不缓存,而不是试图把 Cookie 或 Authorization 全部塞进缓存键:

proxy_cache_bypass $http_authorization $cookie_session;
proxy_no_cache     $http_authorization $cookie_session;

# 动态接口由应用返回
Cache-Control: private, no-store
Vary: Authorization, Cookie

Vary 是否被具体 CDN 纳入缓存键要实际确认,不能只看源站响应头。若业务必须缓存个性化内容,建议只缓存剥离身份信息后的公共片段,并由边缘计算显式构造有限、可审计的缓存键。

3. 日志要能从 CDN 一路关联到源站

边缘节点应生成不可由客户端覆盖的请求 ID,回源时透传;源站同时记录 CDN 请求 ID、实际对端地址、规范化后的客户端地址、缓存状态和上游耗时。Nginx 可以先这样落 JSON 日志:

log_format cdn_json escape=json
  '{"time":"$time_iso8601",'
  '"request_id":"$request_id",'
  '"cdn_id":"$http_x_cdn_request_id",'
  '"peer":"$remote_addr",'
  '"xff":"$http_x_forwarded_for",'
  '"host":"$host","method":"$request_method","uri":"$request_uri",'
  '"status":$status,"bytes":$body_bytes_sent,'
  '"cache":"$upstream_cache_status",'
  '"upstream":"$upstream_addr","urt":"$upstream_response_time",'
  '"ua":"$http_user_agent"}';

access_log /var/log/nginx/cdn_access.json cdn_json;

配置真实 IP 模块时,只信任 CDN 回源网段,否则攻击者直连源站后可伪造客户端地址:

set_real_ip_from 192.0.2.0/24;
set_real_ip_from 2001:db8:100::/48;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

排查时按请求 ID关联 CDN 日志和源站日志;如果同一 ID 只存在于源站,通常是绕过 CDN 或头部伪造;如果只存在于 CDN 且显示缓存命中,则源站没有日志属于正常情况。对疑似直连事件,还应查询安全组流日志或防火墙日志,确认对端地址、目标端口和首次出现时间。

整改后的验收标准可以定得简单一些:公网非 CDN 地址无法访问源站;认证响应不会进入共享缓存;任意一次测试请求都能在边缘日志中定位,并在发生回源时关联到唯一的源站记录。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。