WordPress 放在 CloudFront 和 ALB 后面时,PHP 看到的 REMOTE_ADDR 不是访客的地址。常见的修法是在 wp-config.php 里自己解析 X-Forwarded-For,取“倒数第几个”条目。本站就这样写过,结果访客可以随意指定自己的 IP。原因是官方 WordPress 镜像里的 Apache 已经用 mod_remoteip 处理并改写过这个头。本文按一次请求的实际经过拆开这个问题,再给出改用 CloudFront-Viewer-Address 的修法和它成立的前提。
请求链上的 X-Forwarded-For
假设访客的真实地址是 198.51.100.10,访客在请求里自己带了一个伪造的头。下面的地址都是文档用的示例地址:
GET / HTTP/1.1
Host: www.example.com
X-Forwarded-For: 203.0.113.66 ← 访客自己写的
各环节依次这样处理:
| 环节 | 动作 | 此后的 X-Forwarded-For |
|---|---|---|
| CloudFront | 把 TCP 连接上看到的访客地址追加到末尾 | 203.0.113.66, 198.51.100.10 |
| ALB(默认 append 模式) | 把连到它的客户端,也就是 CloudFront 边缘节点的地址追加到末尾 | 203.0.113.66, 198.51.100.10, 192.0.2.199 |
| Apache mod_remoteip | 见下一节 | 203.0.113.66, 198.51.100.10 |
前两步是 CloudFront 和 ALB 文档里写明的行为。如果只看这两步,“倒数第二个条目是访客”似乎没错:最后一个是边缘节点,倒数第二个是 CloudFront 追加的访客地址,访客没法伪造。本站最初就是这么写的:
// 有问题的写法:再解析一遍 X-Forwarded-For,取倒数第二个
$xff = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
$_SERVER['REMOTE_ADDR'] = $xff[max(0, count($xff) - 2)];
mod_remoteip 已经先处理过一遍
官方 wordpress:*-apache 镜像启用了 mod_remoteip,配置是:
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.0.0.0/8
RemoteIPInternalProxy 172.16.0.0/12
RemoteIPInternalProxy 192.168.0.0/16
RemoteIPInternalProxy 169.254.0.0/16
RemoteIPInternalProxy 127.0.0.0/8
按 Apache 文档,mod_remoteip 从右往左处理这个头;遇到一个不被信任的地址就停下,把它作为客户端地址,并把头改写成“剩下还没确认的地址”;如果全部可信,就删掉整个头。
代入上面的例子:连到 Apache 的是 ALB,地址在 VPC 私有网段里,属于受信任的内部代理。它给出的最右条目 192.0.2.199 是边缘节点的公网地址,不在信任列表里,处理到此为止。结果是:
- Apache 和 PHP 的
REMOTE_ADDR等于192.0.2.199,是 CloudFront 边缘节点; - 传给 PHP 的
X-Forwarded-For已经被改写成203.0.113.66, 198.51.100.10,最右那一项被“用掉”了。
这时 PHP 再取倒数第二个,拿到的是 203.0.113.66,正好是访客伪造的值。访客不带这个头时,改写后只剩 198.51.100.10 一项,max(0, count - 2) 取到下标 0,结果正确。所以正常测试全部通过,只有刻意伪造时才出错。
更早没有 CloudFront、只有 ALB 时,代码取的是最后一项,问题一样:mod_remoteip 已经把 ALB 追加的访客地址用掉了,剩下的最后一项又是访客自己写的。结论是:Apache 前面已经有 mod_remoteip 时,PHP 看到的 X-Forwarded-For 只剩下访客能控制的部分,不应该再解析。
修法:用 CloudFront-Viewer-Address
CloudFront 可以在回源请求里加 CloudFront-Viewer-Address 头,值是访客的 IP 加源端口,例如 198.51.100.10:46532。它不会自动出现,需要回源请求策略把它带上:托管策略 AllViewerAndCloudFrontHeaders-2022-06 包含它,也可以在自定义回源请求策略里单独加。这个头只能放进回源请求策略,不能放进缓存策略。
在 wp-config.php 里(本站通过官方镜像的 WORDPRESS_CONFIG_EXTRA 注入)去掉末尾的端口即可:
if (!empty($_SERVER['HTTP_CLOUDFRONT_VIEWER_ADDRESS'])) {
$_SERVER['REMOTE_ADDR'] = preg_replace('/:\d+$/', '', $_SERVER['HTTP_CLOUDFRONT_VIEWER_ADDRESS']);
}
正则只去掉最后一个“冒号加数字”,所以 IPv6 地址本身的冒号不受影响。这段代码在 wp-config.php 里执行,早于插件加载,插件里的限流、日志、评论记录拿到的都是改过之后的地址。
这个头为什么可以信
单靠换一个头名并不安全,需要两个前提同时成立:
- 值由 CloudFront 生成。AWS 文档说明这个头里是 CloudFront 看到的访客地址和端口。本站 README 记录的结论是,访客自己带的同名头会被 CloudFront 覆盖。AWS 文档里没有找到明确写出“覆盖”的句子,上线前建议在自己的环境里用一个回显请求头的临时页面确认一次。
- 源站只接受来自 CloudFront 的请求。否则攻击者可以绕过 CloudFront 直连 ALB,自己写一个
CloudFront-Viewer-Address。本站的 ALB 规则除了匹配 Host,还要求一个只有 CloudFront 会添加的秘密头,不带这个头的请求落到监听器的默认 404。
只有在第 2 条成立的环境里,才启用上面那行 PHP。本站 CDK 代码里,这行只在配置了 CloudFront 分发时才会生成。
怎么验证
带一个伪造的头请求站点,再看 WordPress 记下的地址,比如在测试文章下发一条评论,到后台看评论者 IP:
curl -s -H 'X-Forwarded-For: 203.0.113.66' https://www.example.com/ -o /dev/null
修复前,后台看到的会是 203.0.113.66;修复后应当是你自己的出口地址。同时要确认绕过 CloudFront 直连源站被拒绝:带正确的 Host、不带秘密头请求 ALB,应该拿到默认动作的响应,而不是 WordPress 页面。
局限 / 注意
- 这行代码只改 PHP 里的
$_SERVER['REMOTE_ADDR']。Apache 访问日志里记录的仍是mod_remoteip得出的边缘节点地址。 - 方案绑定 CloudFront。以后如果在前面再加一层代理或换 CDN,要重新梳理整条链路。
- 另一条思路是让
mod_remoteip信任 CloudFront 的回源地址段,由 Apache 直接得出访客地址。代价是要持续维护这份地址列表,本站没有采用。 - ALB 的
routing.http.xff_header_processing.mode可以设为append、preserve或remove,共享 ALB 上改它会影响所有后端,本站保持默认。



