ThinkPHP 6 的 Request::ip() 默认无条件信任 X-Forwarded-For 或 X-Real-IP 头,完全不校验来源可信性,导致 IP 伪造风险;必须显式配置 trust_proxies 白名单(仅限最靠近 PHP 的可信代理内网段),配合 Nginx 主动覆盖 XFF、启用 realip 模块修正 REMOTE_ADDR,业务层统一调用 request()->ip() 才安全。

ThinkPHP 6 的 Request::ip() 默认不校验 XFF,直接取就危险
默认情况下,Request::ip() 会无条件信任 HTTP_X_FORWARDED_FOR 或 HTTP_X_REAL_IP 头——只要它们存在,就优先返回,完全不检查这个头是不是来自可信代理。攻击者用 cURL 发个 X-Forwarded-For: 127.0.0.1,你的日志、限流、IP 封禁全失效。
这不是“没配对”的问题,而是框架设计上就要求你主动声明信任边界。空的 trust_proxies 白名单 = 全盘接受任意伪造头。
-
trust_proxies必须显式填写,且只填最靠近 PHP 的那一跳代理 IP 段(比如 Nginx 所在机器的内网段) - 填公网 IP 或 CDN 域名(如
cf-ray或slb.aliyuncs.com)无效,Nginx 实际传过来的是真实源 IP,不是域名解析结果 - 阿里云 SLB 场景下,要查 ECS 日志里的
$remote_addr值,填那个私网段(例如10.123.0.0/16),不是控制台显示的 VIP - 填错会导致所有请求 fallback 到
$_SERVER['REMOTE_ADDR'],表现为全量 IP 变成127.0.0.1或统一内网地址
Nginx 层必须做源头净化,否则 ThinkPHP 白名单形同虚设
ThinkPHP 的白名单只管“谁发来的请求可信”,不管“这个请求里带的 XFF 是不是干净”。如果最外层 Nginx 允许客户端随便塞 X-Forwarded-For,那后端哪怕配了白名单也没用——因为伪造头已经混进来了。
关键动作是:对外入口 Nginx 主动覆盖,中间层 Nginx 正确拼接。
立即学习“PHP免费学习笔记(深入)”;
- 对外 Nginx(直连 Cloudflare / 用户浏览器):
proxy_set_header X-Forwarded-For $remote_addr;—— 强制重写,丢弃客户端传来的任何 XFF - 内网中间层(如 Nginx → PHP-FPM):
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;—— 这个变量会把上游传来的 XFF 和自己的$remote_addr拼起来,保证链路可追溯但不引入污染 - 必须启用
realip模块:set_real_ip_from设为可信代理段,real_ip_header X-Forwarded-For,real_ip_recursive on - 禁用
proxy_set_header X-Real-IP $remote_addr的同时又透传原始 XFF —— 这等于开了两个口子,X-Real-IP反而成了绕过白名单的后门
业务代码里别再读 $_SERVER['HTTP_X_FORWARDED_FOR']
直接读 $_SERVER 数组里的代理头,等于绕过 ThinkPHP 所有校验逻辑。哪怕你配了 trust_proxies,$_SERVER['HTTP_X_FORWARDED_FOR'] 依然是原始未过滤值,可能含攻击者注入的逗号分隔 IP 列表。
正确路径只有一条:让 Nginx 用 realip 模块修正 $_SERVER['REMOTE_ADDR'],然后业务中统一用 request()->ip()。
- 控制器里优先调用
$this->request->ip(),它内部已走完白名单校验 + realip 修正流程 - 彻底删除所有类似
$_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']的降级逻辑 - 若需调试,打印
$_SERVER['REMOTE_ADDR']看是否已被 realip 覆盖为真实客户端 IP,而不是看 XFF 头内容 - Swoole 或 FastCGI 环境下,
$_SERVER可能被复用或缓存,$request->header('X-Forwarded-For')也不可靠,唯一可信的是request()->ip()
验证时用 Postman 伪造 XFF,看是否还能生效
配置改完不验证,等于没改。最硬核的验证方式就是主动攻击自己:
- 用 Postman 发请求,手动加头:
X-Forwarded-For: 1.1.1.1, 2.2.2.2, 127.0.0.1 - 观察日志或
request()->ip()返回值:应始终为真实客户端 IP(即你本地出口 IP),而非列表中任意一个 - 如果返回了
1.1.1.1或127.0.0.1,说明 Nginx 没覆盖 XFF,或trust_proxies没生效,或 realip 模块未启用 - 检查 Nginx error.log 是否有
realip: invalid ip类报错,这通常意味着set_real_ip_from段没覆盖到实际代理 IP
多层代理下最容易漏掉的,是只配了最外层 Nginx 的 $remote_addr 覆盖,却忘了中间层也要用 $proxy_add_x_forwarded_for —— 这会导致 XFF 链断裂,realip 模块无法递归解析。



















