CDN背后的盲区:服务器看到了谁?
先看关键判断
如果你正在处理CDN IP,先别急着照搬网上的参数。当你为网站接入CDN(内容分发网络)后,用户的请求不再直接打到你的源站服务器,而是先经过CDN节点。CDN节点缓存内容,或者将请求转发到源站。这时候,你的源站服务器看到的IP地址是CDN节点的IP,而不是真正访问网站的那个用户的IP。
这对于很多业务来说是个大麻烦:你想根据用户IP做访问频率限制、地区白名单、或者记录用户访问日志,结果发现所有请求都来自同一两个CDN节点IP,完全失去了用户维度的信息。
为了补救,HTTP协议里早就定义了一个标准头部——X-Forwarded-For。它的作用就是在请求经过代理(比如CDN)时,把用户的原始IP记录下来,按顺序追加在头部中。于是服务器读取这个头部就能拿到真实IP了。听起来很简单,但问题就出在:这个头部可以被客户端伪造。
进阶阅读:此处可内链到“CDN IP性能优化”指南。
X-Forwarded-For 的工作原理
配置前的检查
先讲清楚X-Forwarded-For是怎么工作的。当用户浏览器直接请求你的服务器时,请求头里没有X-Forwarded-For字段。假设用户的IP是203.0.113.1,经过一个CDN节点后,CDN会在转发给源站的请求中加上一行:
X-Forwarded-For: 203.0.113.1
如果经过多级代理(比如先经过一层负载均衡,再经过CDN),那么每一级代理都会把上一级传来的IP追加在后面,用逗号分隔:
X-Forwarded-For: 用户IP, 第一级代理IP, 第二级代理IP
源站服务器只需要取第一个IP,就是用户的真实IP。
这个机制本身没有问题,很多CDN厂商和负载均衡器都支持。但致命缺陷是:HTTP请求头部是客户端可控的。攻击者发送请求时,可以随意在请求头里带上一个假的X-Forwarded-For字段。
CDN IP:伪造漏洞是怎么发生的?
配置前的检查
假设你的源站服务器代码这样获取用户IP(伪代码):
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
如果攻击者发送一个请求,请求头里已经包含了X-Forwarded-For: 192.168.1.100(一个伪造的内部网段IP),而CDN节点正常转发请求时,通常会追加而不是覆盖已有的X-Forwarded-For。也就是说,最终源站收到的头部可能是:
X-Forwarded-For: 192.168.1.100, 真实攻击者IP
如果你的代码只取了第一个IP(explode(',', $header)[0]),那么你就拿到了错误的内网IP,而攻击者成功隐藏了自己的真实地址。更严重的情况是,如果你的安全策略允许指定IP段访问后台(比如只允许内网IP登录),攻击者就可以伪造一个内网IP来绕过白名单。
这就是X-Forwarded-For伪造漏洞的核心:攻击者可以在源头注入假IP,而服务器无法分辨哪一段是可信的。
延伸阅读:此处可内链到“CDN IP配置案例”相关文章。
为什么不能直接信任X-Forwarded-For?
验证与回滚
很多刚开始接触CDN的朋友会想:“我的CDN节点是固定的,我可以让代码只信任来自这些节点IP的X-Forwarded-For头部,不就行了吗?”这个思路是对的,但需要正确实现。
错误做法是:直接取X-Forwarded-For的第一个IP作为用户IP,而不考虑头部是否被篡改。正确做法是:先判断当前请求是否真的来自你的CDN节点,如果是,则信任该头部并从最后一个(或倒数第二个)取真实用户IP;如果请求不是来自你的CDN节点,那么X-Forwarded-For头部就很可疑,你应该直接忽略它,只用REMOTE_ADDR。
这里有一个关键概念:可信的上游。只有你知道固定的CDN出口IP列表(或者CDN节点IP段),你才能判断当前请求是否经过了你的CDN。如果请求来源IP不在你的CDN节点列表中,那说明可能有人直接访问了源站,或者伪造了CDN回源,这时候X-Forwarded-For头部不可信。
实际中常用的解决方案
下面列出几种常见的正确做法,每个都有适用场景和注意事项。
1. 基于可信IP列表取正确位置的IP
这是最经典的做法。假设你的CDN回源IP是100.0.0.0/8段(真实情况需要向CDN厂商索要)。在服务器层面(Nginx/Apache或应用代码)做如下判断:如果请求来源IP在CDN IP段内,则从X-Forwarded-For的倒数第二个(因为最后一个可能是CDN自身节点IP)取用户IP;如果不在,则直接用REMOTE_ADDR。更精确的做法是:如果X-Forwarded-For头部末尾是当前CDN节点IP,则取前面的IP。
Nginx示例配置:
set_real_ip_from 100.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
这里real_ip_recursive on的作用是递归解引用,从X-Forwarded-For头部中移除最后一个(或最后几个)属于可信IP段的IP,最终剩下的就是真实的客户端IP。这样攻击者注入的假IP会被视为无效,因为只要最后一个IP是可信的CDN节点,前面的就是用户真实IP。
2. 使用CDN厂商提供的专用头部
主流CDN厂商(如Cloudflare、Akamai、阿里云CDN等)通常会把用户真实IP放在一个自定义头部中,这些头部名是固定的且不会被客户端伪造,因为CDN边缘节点会覆盖掉客户端传入的同名头部。例如:
- Cloudflare:
CF-Connecting-IP - Akamai:
True-Client-IP - 阿里云CDN:
Ali-CDN-Real-IP
运维人员只需要配置CDN,让它回源时带上这些头部,然后在源站代码里直接读取对应头部即可。这样做的好处是完全不需要处理伪造问题,因为这些头部在用户请求中即使被带上,也会被CDN节点替换为用户的真实IP。但注意:你需要确保CDN配置正确,并且只信任来自CDN节点IP的请求(防止直接访问源站时这些头部被伪造)。
3. 使用Proxy Protocol
对于更高级的场景(比如四层负载均衡或CDN回源),可以使用Proxy Protocol(代理协议)。它是在TCP层直接携带真实IP,HTTP层不可见,因此完全无法伪造。但需要源站和应用都支持Proxy Protocol协议,配置相对复杂。
关联教程:此处可内链到“CDN IP部署与验证”内容。
补充参考:此处可内链到“CDN IP故障排查实例”。
常见错误与陷阱与CDN IP
验证与回滚
很多开发者会犯以下几个错误,导致IP伪造漏洞依然存在:
- 盲目信任第一个IP:直接取X-Forwarded-For头部逗号分隔后的第一个元素,完全不检查来源。
- 只信任一个固定的IP列表但递归处理错误:比如只设置了
real_ip_header X-Forwarded-For但没开real_ip_recursive,导致Nginx只取最后一个IP,而攻击者如果构造多个IP,最后一个可能是伪造的。 - 在应用层做白名单但用了错误位置:比如在PHP代码里从X-Forwarded-For获取IP,然后判断是否在白名单内,却没有验证请求是否真的来自CDN。
- 混合使用多个头部而没有优先级:比如同时使用X-Forwarded-For和X-Real-IP,但不知道哪个是可靠的。
面向小白的总结:一句话记住
实际操作要点
X-Forwarded-For可以被伪造,所以不能直接相信;正确的做法是让服务器只信任来自你CDN节点IP的请求中的X-Forwarded-For,并且通常取倒数第二个IP(或使用CDN专用头部)。
如果你是新手,最简单的入门方式是:先找到你的CDN厂商提供的回源IP段列表(一般可以在控制台或技术文档中找到),然后在Nginx使用set_real_ip_from配合real_ip_header配置,或者直接使用CDN专用头部。这样不需要修改应用代码,就能安全获取用户真实IP。
记住一点:安全不是靠隐蔽,而是靠验证。清楚哪些IP是可信的,只信任它们携带的信息,才是正确的思路。
延伸:从根源理解HTTP头部伪造
故障定位思路
HTTP协议设计早期并没有充分考虑代理场景下的安全性。X-Forwarded-For最初只是一个非标准头部,后来才被RFC 7239(Forwarded HTTP Extension)标准化。新的标准结构是Forwarded: by=<proxy>; for=<client>;proto=<http/https>,但依然存在同样的问题:客户端可以在第一个代理之前伪造for参数。根本解决方法是:只有第一个可信代理(你的CDN节点)才应该被信任,它需要自己知道直接连接它的客户端IP(来自TCP连接),然后把该IP放入头部,同时覆盖或忽略客户端自带的头部。
大多数CDN厂商确实这么做了:如果客户端携带了伪造的X-Forwarded-For,CDN边缘节点会将其追加到现有头部之后(而非替换),但最终回源时用户看到的头部顺序可能取决于具体实现。因此,理解你的CDN节点的行为并正确配置回源策略,比死记硬背代码更重要。
希望这篇文章能帮你彻底搞懂CDN获取真实IP的来龙去脉,下次再遇到类似问题,你不仅能修bug,还能解释清楚背后的原理。后续只要定期检查关键指标,CDN IP就不会变成维护负担。
延伸阅读
