资讯文章 资讯 · NEWS
反代后面那个协议头:X-Forwarded-Proto 配错之后,目录站会出哪些怪事
2026-10-24 约 2,249 字 预计阅读 6 分钟 yiyunkj
反
TLS 在边缘终结是很常见的部署:浏览器用 HTTPS 连到反向代理或 CDN,代理再用 HTTP 把请求转给后端应用。后端看到的连接永远是 HTTP。
如果应用根据连接协议做决定——要不要重定向到 HTTPS、生成绝对 URL、给 Cookie 加 Secure——它就会基于错误信息做出错误判断。

图片来源:网络,版权归原作者所有,侵权请联系删除
最常见的症状:重定向循环
应用发现请求是 HTTP,返回 301 到 HTTPS。浏览器重新请求 HTTPS,代理转成 HTTP 转给应用,应用又判定是 HTTP,再 301。浏览器弹 ERR_TOO_MANY_REDIRECTS。
浏览器有上限,Chrome 大约在 20 次跳转后放弃;curl 默认跟 50 次。所以看到这个错误时,链条其实已经在那里转了十几圈了。
修法是让应用看转发的协议头,而不是看自己这条连接。代理侧:
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
关键是 $scheme 而不是写死 https——它反映的是客户端实际到达代理时用的协议。同时应用侧必须显式声明信任这个头,不同框架写法不同:
# Django
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Express
app.set('trust proxy', 'loopback') # 或按实际网络拓扑设置
# Apache(容器内的 PHP 应用常见)
SetEnvIf X-Forwarded-Proto "https" HTTPS=on
第二种症状:页面里全是 http 绝对地址
这个更隐蔽,因为它不报错,只是页面在 HTTPS 下加载了 http 的子资源,浏览器拦掉,样式和功能部分失效。
对目录站来说影响比一般站点大:canonical、og:image、sitemap 里的地址、结构化数据里的 url,全都是从应用生成的绝对地址。协议写错意味着这些信号全部指向一个不同的地址。
检查一下页面上这几处,用 curl 直接看源站输出最准:
curl -s https://example.com/item/foo | grep -o 'https\?://[^"]*' | sort -u
如果里面混着 http 的地址,说明生成逻辑在直接读连接协议。
第三种症状:CDN 上缓存了两个版本
代理侧配了重定向,应用侧也配了重定向,两层的规则不一致,甚至同一层里有两条规则互相打。这类问题在 CDN 和 nginx 各自有一套跳转规则时特别常见。
还有一种特殊情况:CDN 的 SSL 模式设成 Flexible(访客到 CDN 是 HTTPS,CDN 到源站是 HTTP),而源站自己有 HTTP 转 HTTPS 的规则。这样 CDN 每一次回源都会被重定向,最终访客被反复弹。修法是把加密模式设为 Full 或 Full (strict),而不是把源站的重定向删掉。
第四种症状:缓存了错误的跳转
规则改对了,问题还在。这时候要清缓存——CDN 缓存的旧 301 会一直生效到过期。浏览器本地缓存的跳转也一样,排查时用无痕窗口或者 curl 绕过。
还有个容易忽略的:浏览器本地的 HSTS 状态。如果之前下发过 max-age 很长的 Strict-Transport-Security,浏览器会在本地强制把 http 请求改写成 https,跟服务器配置无关。排查时要先把这个变量排除掉。
信任边界:这个头不能乱信
X-Forwarded-Proto 是事实标准,不是 HTTP 规范的一部分(规范里对应的是 Forwarded)。它可以被客户端伪造。
规则只有一条:只有在代理会覆盖这个头的前提下才信任它。如果客户端能直接访问到应用,它带一个 X-Forwarded-Proto: https 就能骗过所有基于这个头的安全判断。
验证方法很简单,从外面发一个伪造头的请求:
curl -i -H 'X-Forwarded-Proto: http' https://example.com/probe
如果返回的结果跟着伪造值变了,说明配置有问题。正常情况下代理会覆盖客户端传来的值,应用应该始终看到代理写入的那个。
另一个边界是 Host 头。不要用未校验的 Host 拼重定向目标——那样等于让请求方决定跳到哪去。用配置里写死的公开域名,只从请求里取路径和查询串。
多层代理时怎么数
CDN → nginx → 应用,这是两层。用数字表示信任跳数(比如 Express 的 trust proxy 2)在这种场景下是对的,但要注意:数字是跳数不是白名单,不同路径经过的代理层数可能不一样。能按网络地址精确指定时优先精确指定。
如果 CDN 在 nginx 之前终结 TLS,那么 nginx 里的 $scheme 描述的是 CDN 到 nginx 这一段,不是访客到 CDN 那一段。这时候要用 CDN 提供的协议信号,各家的头名不一样,查文档确认。
目录站的检查清单
- 代理侧用 $scheme 转发,不要写死。
- 应用侧显式声明信任,不要只依赖框架默认值。多数框架默认只看自己的连接。
- 只在 HTTP 监听块里做跳转。把 return 301 写在同时监听 443 的 server 块里,会让 HTTPS 请求自己跳自己。
- 定期验一次伪造头。配置变更后这条最容易漏。
- canonical、og:image、sitemap 用同一套生成逻辑。不要各处各写一遍,协议错了就全错。