SSRF(Server-Side Request Forgery,服务端请求伪造)是一种利用服务端代为发起请求的攻击方式。攻击者通过构造恶意URL,诱导服务器去访问本不该暴露的内网资源。在带有CDN的架构中,客户端请求先经过CDN边缘节点回源到源站,如果源站上还存在其他基于URL发起请求的功能(比如图片抓取、网页预览、Webhook回调),一旦过滤不严,攻击者就能借助这些功能探测甚至渗透内网。本文将围绕内部IP地址过滤这一核心手段,系统讲解CDN侧SSRF防护的实现细节。

一、为什么CDN架构下SSRF风险更高
很多团队认为加了CDN之后,源站隐藏在CDN后面,安全风险会降低。事实恰恰相反,CDN架构反而放大了SSRF的三个风险点。首先,源站为了识别真实客户端IP,通常会从X-Forwarded-For、CF-Connecting-IP、X-Real-IP等请求头中提取地址,如果信任链没有做好,攻击者可以伪造这些头部绕过访问控制。其次,源站与CDN回源节点之间往往处于同一内网或专线网络,一旦SSRF成立,攻击者可以直接访问到CDN管理接口、回源调度服务等敏感内网资产。最后,云环境下源站往往部署在VPC内,通过169.254.169.254这类元数据地址可以获取临时凭证,SSRF一旦打通,危害会从内网探测升级为云账号失陷。
举一个典型场景:站点提供网址缩略图功能,用户提交一个URL,服务器抓取该URL生成截图。攻击者提交http://169.254.169.254/latest/meta-data/,如果服务器没有校验目标地址,云平台的实例元数据就会直接返回给攻击者。这类案例在公开漏洞报告中屡见不鲜,防护的第一道闸门就是对请求目标做内部IP过滤。
二、需要过滤的IP地址范围清单
做好内网IP过滤,第一步是明确哪些地址属于内部或保留地址。只判断192.168.、10.、172.16-31.这三个私有网段是远远不够的,攻击者常用的绕过手段还包括环回地址、链路本地地址、保留地址以及各种特殊表示形式。完整的过滤清单应该至少覆盖以下范围。
- 回环地址:127.0.0.0/8,包括127.1、127.0.0.1等各种变体
- 私有地址:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16
- 链路本地地址:169.254.0.0/16,云元数据服务就在这个网段
- 保留地址:0.0.0.0/8、192.0.2.0/24、198.51.100.0/24、203.0.113.0/24
- 组播与广播地址:224.0.0.0/4、255.255.255.255
- IPv6相关:::1、fc00::/7、fe80::/10、::ffff:映射地址
以Python为例,借助内置的ipaddress模块可以写出简洁可靠的判断逻辑,避免手写字符串匹配带来的遗漏。
import ipaddress
# 内部与保留网段黑名单
PRIVATE_NETWORKS = [
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('169.254.0.0/16'),
ipaddress.ip_network('0.0.0.0/8'),
ipaddress.ip_network('224.0.0.0/4'),
ipaddress.ip_network('::1/128'),
ipaddress.ip_network('fc00::/7'),
ipaddress.ip_network('fe80::/10'),
]
def is_internal_ip(host: str) -> bool:
"""判断目标IP是否属于内部或保留地址"""
try:
addr = ipaddress.ip_address(host)
except ValueError:
return False # 不是合法IP,交给DNS解析后再校验
return any(addr in net for net in PRIVATE_NETWORKS)注意这里传入的必须是解析后的IP,而不是用户提交的原始字符串。直接对原始字符串做判断是初学者最常见的错误,因为127.1、0177.0.0.1、2130706433这类非常规写法都能被浏览器和网络库解析为回环地址,却不会被简单的字符串前缀匹配拦截。
三、DNS重绑定攻击与解析时机问题
只做一次IP校验仍然存在漏洞,最典型的是DNS重绑定攻击。攻击者先申请一个域名,配置一条TTL极短的A记录,第一次解析返回公网IP通过校验,等到服务器真正发起请求时,第二次解析返回内网IP,校验就被绕过了。这种攻击利用的是校验与请求之间的时间差,防护上必须保证两次DNS解析结果一致。
稳妥的做法是在DNS解析完成后直接使用解析得到的IP发起连接,并把Host头设置为原始域名,这样后续请求不再依赖第二次解析。下面是一个基于Go语言的示例,展示如何固定解析结果发起请求。
package main
import (
"context"
"net"
"net/http"
"time"
)
func safeGet(rawURL string) (*http.Response, error) {
host, port, err := net.SplitHostPort(parseHost(rawURL))
if err != nil {
return nil, err
}
// 第一次解析并校验
addrs, err := net.DefaultResolver.LookupIP(context.Background(), "ip", host)
if err != nil || len(addrs) == 0 {
return nil, err
}
for _, addr := range addrs {
if isInternalIP(addr.String()) {
return nil, ErrBlockedIP
}
}
// 使用已校验的IP建立连接,锁定解析结果
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
target := net.JoinHostPort(addrs[0].String(), port)
return dialer.DialContext(ctx, network, target)
},
}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
req, _ := http.NewRequest("GET", rawURL, nil)
req.Host = host // 保留原始Host,兼容虚拟主机站点
return client.Do(req)
}除了重绑定,还要警惕URL解析层面的绕过:限制协议只允许http和https、禁止URL中出现userinfo部分(例如http://expected.com@127.0.0.1/这种写法实际访问的是127.0.0.1)、禁止重定向或对重定向后的地址重新执行完整校验。很多SSRF漏洞的复现和修复,反复拉锯的就是这几个细节。
四、网络层兜底与纵深防御建议
应用层的IP过滤是第一道防线,但不应该是唯一一道。攻击面总是不断变化,解析库的怪癖、新出现的地址表示形式都可能让过滤规则失效,因此建议叠加网络层控制。最直接的措施是给发起外部请求的服务配置专门的出口,通过安全组或防火墙策略禁止其访问内网网段和元数据地址,只放行80和443端段的公网出口。这样即使应用层校验被绕过,请求也无法到达内网目标。
对于云环境,还要注意关闭或加固元数据服务。主流云厂商都支持为实例元数据开启强制Token模式,开启后必须携带临时Token才能访问,单靠SSRF拿不到凭证。此外,日志层面建议记录所有出站请求的目标地址,出现对内网网段的探测行为时能快速定位受害接口。条件允许的情况下,可以把抓取类功能隔离到独立的无网络 privileged 容器中执行,通过代理白名单转发请求,进一步压缩攻击者的活动空间。
总结来看,CDN侧SSRF防护的核心在于三点:用完整的网段清单校验解析后的真实IP、通过固定解析结果对抗DNS重绑定、再用网络层策略兜底。三层措施相互独立又层层递进,任何一层被突破,其余两层仍能守住底线。安全从来不是单点能力,把内网访问控制做成纵深体系,才是应对SSRF这类漏洞的长久之计。