SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可控 URL”这么简单。真正影响风险等级的因素包括:请求由哪个组件发起、是否能访问内网地址、是否跟随重定向、是否携带敏感凭据,以及应用对响应内容如何处理。本文以代码审计和修复验证为主线,整理一套适用于 Web 服务的 SSRF 分析方法,重点放在可复现、可解释和可持续修复上。
一、问题背景与典型场景
常见的 SSRF 入口包括图片抓取、Webhook、在线文档转换、URL 预览、远程文件导入、第三方回调测试等功能。它们通常具有相似的数据流:
- 用户提交一个 URL 或主机名;
- 服务端对其进行 DNS 解析、建立连接并发送请求;
- 服务端将响应保存、转换、解析或回显给用户。
风险并不只来自访问云平台元数据。即使没有云环境,服务端请求能力也可能触达管理后台、内部监控、数据库代理、调试接口或仅绑定在回环地址上的服务。因此审计时应先识别“请求能力”和“网络边界”,再判断具体影响。
二、审计时先画清数据流
建议不要一开始就围绕某个 Payload 反复尝试,而是先回答以下问题:
- 输入点在哪里?是完整 URL、主机名、路径,还是由多个参数拼接而成?
- URL 在哪里被解析?是否存在二次解析、重定向或代理转发?
- 实际发起请求的库是什么?是否默认支持 HTTP、HTTPS 以外的协议?
- 请求使用什么网络身份?是否带有内部 Token、Cookie 或云环境凭据?
- 响应是否会回显、落盘、进入 XML/JSON/图片解析器?
- 请求是否受到连接超时、响应大小和并发数量限制?
以 Java 服务为例,审计重点通常不是单个调用点,而是从 Controller、Service 到 HTTP 客户端的完整链路。可以优先搜索 URL 参数的流向,以及常见客户端调用,例如 URLConnection、HttpClient、OkHttp、WebClient、RestTemplate 等。
String target = request.getParameter('url');
return httpClient.get(target);如果中间只做了空值判断、后缀判断或简单字符串替换,通常不能视为有效防护。URL 的语义解析不能依赖字符串前缀匹配,因为端口、凭据、编码、重定向和 DNS 解析都会改变最终连接目标。
三、URL 校验中最容易被忽略的细节
1. 解析结果与原始字符串不一致
安全判断应基于标准 URL 解析器得到的结构化字段,而不是对原始字符串执行 startsWith 或 contains。至少需要分别处理协议、主机名、端口、用户名信息和路径。
2. DNS 解析与连接之间存在时间窗口
即使校验阶段确认域名解析到公网地址,真正建立连接时也可能得到不同结果。尤其是在允许用户控制域名或存在 DNS 变化的场景中,仅在校验阶段解析一次并不可靠。更稳妥的方式是使用受控的出站代理,在代理侧统一完成解析、连接和目标地址校验。
3. 重定向会绕过一次性校验
初始 URL 可能指向允许访问的地址,但服务端跟随 30x 跳转后进入内部网络。因此应默认关闭自动重定向;如果业务必须支持,应对每一次跳转重新执行协议、地址范围和端口策略校验,并限制最大跳转次数。
4. IPv4、IPv6 与特殊地址范围
不能只拦截 127.0.0.1。实际策略应覆盖回环地址、私有地址、链路本地地址、未指定地址、组播地址、保留地址以及 IPv6 对应范围。还要注意 IPv4 映射的 IPv6 地址,以及不同文本形式在解析后的等价性。
四、推荐的防护实现
业务允许的情况下,最安全的方案不是“允许所有地址,再维护黑名单”,而是建立严格的出站白名单:限定协议为 HTTPS,限定端口,限定目标域名后缀或完整域名,并通过独立的 egress 代理访问外部资源。
如果确实需要支持用户输入任意公网 URL,可以将校验拆分为几个步骤:
- 只允许业务需要的协议,例如 HTTPS;
- 拒绝 URL 中的用户名和密码字段;
- 明确处理默认端口与显式端口;
- 解析所有 A 和 AAAA 记录,并拒绝解析到受限地址的目标;
- 建立连接前再次确认最终地址;
- 关闭或严格控制重定向;
- 限制连接超时、读取超时、响应体大小和下载时间;
- 不把内部响应原样回显给普通用户。
下面是一个用于说明策略的 Python 伪代码。实际项目中应结合框架的网络库和统一出站代理实现,不能直接复制后视为完整安全组件。
from urllib.parse import urlparse
import ipaddress
import socket
def is_public_address(address):
ip = ipaddress.ip_address(address)
return not (ip.is_private or ip.is_loopback or ip.is_link_local or
ip.is_reserved or ip.is_multicast or ip.is_unspecified)
def validate_target(raw_url):
parsed = urlparse(raw_url)
if parsed.scheme not in ('https',):
raise ValueError('scheme is not allowed')
if not parsed.hostname or parsed.username or parsed.password:
raise ValueError('invalid host information')
if parsed.port not in (None, 443):
raise ValueError('port is not allowed')
addresses = socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM)
for item in addresses:
address = item[4][0]
if not is_public_address(address):
raise ValueError('resolved address is not allowed')
return parsed这段示例仍然存在 DNS 竞态问题,因此更适合作为审计思路展示。生产环境应让请求经过固定的出站网关,由网关在同一网络路径中完成解析、连接和策略检查,应用自身只接收经过策略过滤的结果。
五、如何设计安全的复现与验证
漏洞验证应在授权测试环境中完成,优先使用自建的测试服务,不要直接探测生产内网、云平台管理地址或第三方系统。建议准备以下组件:
- 一个可记录请求方法、Host、路径和来源地址的外部测试服务;
- 一个仅绑定在测试机回环地址的模拟内部服务;
- 一个返回重定向的测试端点;
- 一个解析较大响应或延迟响应的测试端点,用于验证超时和大小限制。
验证重点不是获取敏感数据,而是确认策略是否真正生效:
- 访问明确允许的测试域名时,请求能够正常完成;
- 访问回环、私有和链路本地地址时,请求在连接前被拒绝;
- 域名解析到受限地址时,请求被拒绝;
- 跳转到受限地址时,第二跳被阻断;
- 超时和超大响应不会长期占用工作线程或内存;
- 错误信息不会泄露内部 IP、完整响应头或网络拓扑。
可以在测试服务端记录如下字段,便于判断防护发生在什么阶段:
timestamp
request_id
source_service
requested_host
resolved_address
scheme
port
redirect_count
blocked_reason
response_size
elapsed_ms六、代码审计中的常见误区
- 只检查 URL 是否以 https 开头,却没有检查解析后的主机和端口。
- 只拦截 127.0.0.1,没有覆盖 IPv6、私有网段和链路本地地址。
- 校验一次后允许客户端自动跟随重定向。
- 使用黑名单维护所有内部网段,却忽略新的网络环境和地址表示方式。
- 把 SSRF 仅当作读取文件的问题,忽略内部管理接口上的状态修改操作。
- 只修复一个 Controller,没有排查项目内其他 URL 抓取功能。
- 修复后只做正向测试,没有验证超时、异常、重定向和多地址解析。
七、纵深防御与监控
应用层校验不是唯一防线。生产环境建议同时配置出站网络策略,默认禁止业务容器访问不必要的内网网段和管理端口。对于确需访问外部资源的服务,使用独立的网络身份和最小权限凭据,避免请求进程能够直接访问云环境敏感接口。
日志方面,建议记录请求 ID、目标域名、解析地址、最终连接地址、阻断原因、响应大小和耗时,但不要记录完整 Authorization、Cookie 或响应正文。监控上可以关注短时间内大量不同目标、频繁解析失败、访问受限网段、异常长连接和重定向次数突增等信号。
八、修复验收清单
| 检查项 | 验收标准 |
| 协议 | 仅允许业务必需协议,禁止不必要的本地文件或特殊协议 |
| 主机 | 使用结构化解析和明确白名单,禁止凭据字段 |
| 地址 | 覆盖 IPv4、IPv6 及所有受限地址范围 |
| 解析 | 校验与实际连接路径一致,避免 DNS 竞态 |
| 重定向 | 默认关闭,开启时逐跳校验并限制次数 |
| 资源 | 设置连接、读取、响应大小和并发限制 |
| 网络 | 通过出站策略限制容器或主机可访问范围 |
| 日志 | 可审计但不记录敏感凭据和完整响应内容 |
总结
SSRF 的核心不是某个固定绕过技巧,而是服务端把用户输入转化为网络请求后,边界控制是否完整。高质量的审计应沿着“输入解析—地址解析—连接建立—重定向—响应处理—网络权限”逐层检查;高质量的修复则应结合应用白名单、出站代理、网络隔离、资源限制和审计监控。把验证重点放在安全边界是否被突破,而不是敏感数据是否被取出,通常更容易形成可复用、可验收的长期防护方案。
推荐意见