SSRF(Server-Side Request Forgery,服务器端请求伪造)漏洞的可怕之处在于,它把服务器变成了攻击者的跳板。你以为你在防护一个Web应用,实际上你在防护整个内网。修复SSRF不能只靠简单的黑名单,因为攻击者绕过的技巧远比你想象的多。直接讲核心逻辑:SSRF的根治方案是“默认拒绝,按需放行”,并且要结合网络层、应用层、业务层的多层防御体系。
彻底理解URL解析的混乱性很多防护失败,根源在于对URL解析器的理解不够。攻击者利用不同组件对URL解析的差异来绕过限制。比如,你封禁了127.0.0.1,攻击者可能会使用http://2130706433/(十进制IP)、http://0x7f.0.0.1/(十六进制IP)或者http://127.0.0.1.nip.io/(DNS重绑定)。更隐蔽的是利用URL中的用户信息部分,例如http://safe-domain.com@127.0.0.1/。很多解析器会把这个URL的主机部分识别为safe-domain.com,但实际请求却发往127.0.0.1。还有利用中文句号、下划线等Unicode字符在特定后端语言中的规范化处理差异进行绕过。因此,任何基于字符串匹配的黑名单防护,在没有对URL进行标准化解析之前,都是形同虚设的。
第一道防线:网络层的物理隔离这是最硬核、最不容易被绕过的防护手段。如果你的应用服务器根本不需要访问内网资源,那就应该在网络层面彻底阻断它访问内网的可能性。具体操作是:将所有需要发起外部请求的服务,单独部署在一个隔离的“外联区”或“DMZ区”内。通过安全组或防火墙策略,严格限制该区域内服务器的出站流量,只允许它们访问特定外部目标(如特定的第三方API域名或IP段),而禁止访问任何内网地址段(如10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)。即使攻击者找到了应用层的SSRF漏洞,他发出的请求也会被物理防火墙直接丢弃,漏洞利用链在第一环就被斩断。这是最值得投入的防御成本。
应用层的“默认拒绝”策略当业务必须允许用户提供URL或IP地址时,比如一个自定义头像上传、网页截图或数据采集功能,应用层的逻辑必须从“黑名单”思维彻底转变为“白名单”思维。不要去想“哪些不能访问”,而要去想“哪些可以访问”。
第一步,协议限制。只允许http和https协议,直接禁用file://、gopher://、dict://、ftp://等所有其他协议。这一步很简单,但能直接封死一大类攻击手法。
第二步,DNS解析与校验。这是核心步骤,也是最容易出错的地方。流程应该是:获取用户输入的URL -> 解析出主机名 -> 进行DNS解析,获取所有A记录和AAAA记录 -> 检查每一个解析出的IP地址是否在黑名单中。这里的黑名单是内网IP段和回环地址。注意,你必须检查所有解析出的IP,而不仅仅是第一个。攻击者可以配置一个域名同时解析到一个外网合法IP和一个内网IP,如果你的代码只检查了第一个IP就放行,那么后续请求库可能因为连接失败而自动尝试第二个IP,从而击中内网。
# 一个严谨的DNS校验示例(Python伪代码)
import socket
import ipaddress
def is_ip_internal(ip_str):
"""检查IP是否为内网或回环地址"""
try:
ip = ipaddress.ip_address(ip_str)
return ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local
except ValueError:
return True # 解析失败,视为不安全
def validate_host(hostname):
"""校验主机名,返回是否安全"""
try:
# 获取所有IP地址,包括IPv4和IPv6
infos = socket.getaddrinfo(hostname, None)
for info in infos:
ip = info[4][0]
if is_ip_internal(ip):
return False
return True
except socket.gaierror:
# 无法解析的域名,直接拒绝
return False
# 使用示例
user_url = "http://example.com@127.0.0.1/"
# ... 解析出主机名 hostname = "127.0.0.1" ...
if not validate_host(hostname):
raise ValueError("Access to internal network is forbidden.")
第三步,处理HTTP重定向。这是极易被忽略的盲区。你的初始请求可能通过了所有安全检查,目标服务器返回一个301或302重定向,将你的服务端请求指向了内网地址。因此,你使用的HTTP客户端库必须禁用自动重定向,或者实现一个自定义的重定向处理逻辑:在每一次跟随重定向之前,必须对新的目标URL重新执行一遍完整的DNS解析和IP校验流程。不校验重定向目标的SSRF防护,等于没做。
构建坚不可摧的代码实现在实际代码中,不要自己去写URL解析器,而是使用成熟、经过安全测试的库,并正确使用它们。以Java为例,使用java.net.URI而不是java.net.URL,因为后者在解析时会进行DNS查询,可能引发TOCTOU(检查时间到使用时间)问题。拿到URI对象后,获取其主机名,然后单独进行DNS解析和校验。对于Go语言,要小心net/http包默认的自动重定向行为,务必设置自定义的CheckRedirect函数。对于Python的requests库,设置allow_redirects=False,并手动处理重定向循环。
一个完整的请求函数应该包含以下步骤:解析URL -> 校验协议 -> 提取主机名 -> 独立进行DNS解析 -> 遍历校验所有IP -> 发起请求(禁用自动重定向) -> 检查响应状态码 -> 如果是重定向,对新URL重复以上所有步骤 -> 返回最终响应内容。这个过程虽然繁琐,但每一步都是必要的。
业务层的纵深防御与盲打补丁即便网络和应用层做得再好,业务层仍然可以增加一道防线。永远不要在客户端或前端传递完整的后端资源URL,而是传递一个资源ID或经过签名的令牌。例如,一个文件下载功能,前端不应该传递file_url=http://internal-server/data.pdf,而应该传递file_id=12345。后端根据这个ID从数据库或配置中查找对应的、经过授权的资源路径。这就从根本上杜绝了用户控制URL的可能性。
对于无法避免需要用户传入目标地址的场景,比如网站监控服务,可以引入“请求代理”或“任务队列”模式。用户提交的URL不是由核心业务服务器直接去请求,而是被放入一个消息队列。由一台或多台专门部署在隔离网络中的、没有任何内网权限的Worker服务器去消费这些任务。Worker服务器甚至可以被设置为只读文件系统,并且每次任务执行后都进行销毁和重建,以保持环境的绝对纯净。即使Worker被SSRF攻击命中,它也无内网可达,无敏感信息可泄露。
常见绕过技巧与应对措施清单攻击者的创造力是无穷的,你需要了解他们的常用手法,才能构建更完善的防护。除了前面提到的IP进制转换和URL用户信息混淆,还有利用IPv6地址的映射,例如http://[::ffff:127.0.0.1]/。利用DNS重绑定,攻击者拥有一个域名,第一次解析返回一个合法外网IP,通过你的校验后,第二次解析迅速变为内网IP。对抗DNS重绑定,可以在校验IP后,将域名和其解析出的IP进行绑定,在后续请求时直接使用这个IP,并设置自定义的Host头,避免再次DNS解析。或者,在发起请求前和请求后都做一次校验,但这样性能开销较大且仍有竞态条件风险。
利用云服务的内网地址也是常见手法。在AWS、阿里云、腾讯云等环境中,有一个特殊的元数据地址169.254.169.254,这个IP能获取到服务器的角色、临时凭证等高敏感信息。这个地址必须被明确加入黑名单。此外,云服务商的内网DNS名称和IP段也需要一并封禁。利用302跳转绕过,以及利用CRLF注入在HTTP头中注入恶意内容来操控请求,这些都需要在代码层面进行严格的输入过滤和输出编码。
监控与告警:发现盲区的眼睛再完美的防护体系也可能存在未知的盲区。你需要建立有效的监控和告警机制。在应用日志中,记录所有由服务端发起的对外请求,包括目标URL、解析后的IP、响应状态码和重定向链。对任何请求内网IP地址的行为,即使被拦截,也要发出告警,因为这可能意味着有人在探测你的系统。分析请求模式,如果某个功能点突然开始请求大量不同域名,可能是扫描器在寻找SSRF点。监控出站流量,网络层的防火墙日志可以与应用日志进行关联分析,发现应用层漏掉、但被网络层拦截的请求,这直接指示了防护策略的缺口。
SSRF的防护不是一劳永逸的,它是一个持续对抗的过程。你需要定期审计代码,使用自动化扫描器进行内部测试,并关注新公开的绕过技术和漏洞模式。将你的防护逻辑封装成一个经过严格测试的安全库,供全公司所有业务线使用,这样当新的绕过手法出现时,你可以在一处升级,全局生效。最终,SSRF防护的成败,取决于你对“输入是不可信的”这一原则的坚守程度,以及你在网络、应用、业务三个层面构筑的防御纵深。
