SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替用户发一次请求”这么简单。真正危险的地方在于,服务端所在网络通常拥有比普通用户更高的访问权限,可能能够连接内部管理端口、云平台元数据服务、仅监听本机的调试接口,甚至访问带有内部信任关系的业务系统。
在实际审计中,SSRF 往往藏在图片抓取、Webhook、文件导入、URL 预览、PDF 生成、在线检测等功能里。单纯依靠关键字搜索容易漏报,也容易把正常的 HTTP 客户端误判为漏洞。比较可靠的方法,是从数据流入手,结合 URL 解析、DNS 解析、重定向处理和网络出口策略进行完整分析。
一、问题场景与风险边界
典型场景是后端接收一个由用户控制的 URL,然后在服务器端发起请求。例如:
String target = request.getParameter("url");
String body = restTemplate.getForObject(target, String.class);
return ResponseEntity.ok(body);这段代码的问题不在于使用了哪一个 HTTP 客户端,而在于请求目标完全由外部输入决定,且没有建立明确的访问边界。攻击者可能尝试让服务端访问:
- 本机回环地址或本机监听的管理接口;
- 企业内网中的非公开服务;
- 云环境中的实例元数据服务;
- 开发阶段遗留的调试端点;
- 能够触发敏感操作的内部 API。
在授权测试和代码审计中,应将验证范围限制在自有测试环境、明确授权的资产和无敏感数据的模拟服务上。不要把真实云环境、生产内网或第三方服务作为验证目标。
二、审计时如何定位 SSRF 数据流
第一步不是搜索“SSRF”三个字,而是寻找“用户输入进入网络请求”的完整路径。常见入口包括以下几类:
- 请求参数:url、link、callback、webhook、source、redirect 等;
- JSON 或 XML 字段中的远程地址;
- 上传文件中的外部资源引用,例如 HTML、SVG、Markdown;
- 后台任务、消息队列或定时任务中的抓取地址;
- 第三方 SDK 的 URL 预览、图片下载和文档转换功能。
随后需要继续追踪数据是否经过以下处理:
- 是否进行了 URL 解析,还是仅用字符串前缀判断;
- 是否校验了协议、主机名、端口和路径;
- 是否在校验后重新解析 DNS;
- 是否允许自动跟随重定向;
- 是否存在代理、DNS 缓存或多网卡导致的出口差异;
- 响应内容是否直接返回给用户,还是仅作为后台任务使用。
对于 Java 项目,可以重点搜索 URL、URI、HttpClient、OkHttp、RestTemplate、WebClient、URLConnection 等调用点。Python 项目则应关注 requests、urllib、httpx、aiohttp 等库。搜索只是起点,必须继续确认数据是否可控以及请求是否真正从服务端发出。
三、一个容易被忽略的解析问题
很多修复方案只做了简单的字符串判断,例如判断目标地址是否以“ example.com,因为 URL 语法中还存在用户信息、端口和主机名等部分。
正确做法是先使用标准库解析 URL,再对结构化字段进行校验。下面是一个简化的 Python 示例,重点是说明校验顺序,实际项目仍应结合业务白名单和网络出口策略:
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_HOSTS = {"static.example.com", "cdn.example.com"}
ALLOWED_SCHEMES = {"https"}
def is_public_ip(value: str) -> bool:
try:
address = ipaddress.ip_address(value)
return not (
address.is_private
or address.is_loopback
or address.is_link_local
or address.is_reserved
or address.is_multicast
or address.is_unspecified
)
except ValueError:
return False
def validate_target(raw_url: str) -> str:
parsed = urlparse(raw_url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError("unsupported scheme")
if not parsed.hostname:
raise ValueError("missing hostname")
if parsed.username or parsed.password:
raise ValueError("userinfo is not allowed")
if parsed.hostname.lower().rstrip(".") not in ALLOWED_HOSTS:
raise ValueError("host is not allowed")
# 仅用于说明风险检查;生产代码还需要处理 DNS 解析失败、IPv6 等情况
resolved = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
for item in resolved:
ip = item[4][0]
if not is_public_ip(ip):
raise ValueError("resolved address is not public")
return parsed.geturl()这个示例仍然不能单独解决 DNS 重绑定问题。原因是校验阶段解析出的地址,与实际建立连接时使用的地址可能不是同一个。更稳妥的实现方式是让解析、地址过滤和连接建立使用同一套受控解析逻辑,或者将抓取服务放在独立的网络隔离区,并通过出口代理执行最终访问。
四、为什么“只禁止内网 IP”仍然不够
SSRF 防护经常被简化成“解析域名,发现是内网地址就拒绝”。这比完全不校验要好,但仍然有几个常见缺陷。
1. DNS 解析与连接时机不一致
如果应用先解析域名做检查,随后再把原始域名交给 HTTP 客户端建立连接,客户端可能重新解析该域名。两次解析结果不同,就可能导致校验和实际连接不一致。
2. 重定向绕过初始校验
应用校验了第一个 URL,但 HTTP 客户端自动跟随重定向后,最终请求可能落到未允许的目标。防护策略应明确是否允许重定向。安全性要求较高时,建议默认禁止;确有业务需要时,应对每一次重定向重新执行完整校验,并限制重定向次数。
3. IPv4 与 IPv6 处理不完整
只检查 IPv4 私有网段会遗漏 IPv6 回环地址、链路本地地址和其他特殊地址。地址判断应使用标准 IP 解析库,而不是依赖正则表达式。
4. 代理配置改变了真实出口
HTTP 客户端可能继承环境变量或框架级代理配置。审计时需要确认请求最终经过哪里,不能只看应用代码中的 URL。容器、Sidecar、服务网格和企业代理都可能改变网络路径。
五、推荐的安全实现方式
如果业务允许,最可靠的策略通常是“目的地白名单”,而不是试图穷举所有危险地址。白名单至少应包含协议、主机、端口和必要的路径范围,并尽量避免允许用户直接控制端口。
public URI validateTarget(String raw) {
URI uri = URI.create(raw);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new IllegalArgumentException("only https is allowed");
}
if (uri.getUserInfo() != null) {
throw new IllegalArgumentException("userinfo is not allowed");
}
String host = uri.getHost();
if (host == null || !host.equalsIgnoreCase("cdn.example.com")) {
throw new IllegalArgumentException("host is not allowed");
}
int port = uri.getPort();
if (port != -1 && port != 443) {
throw new IllegalArgumentException("port is not allowed");
}
return uri;
}如果业务无法使用固定域名白名单,例如需要抓取客户配置的站点,则建议采用分层防护:
- 仅允许 http 或 https,拒绝 file、ftp、gopher 等无业务必要的协议;
- 拒绝用户名、密码、异常端口和不符合规范的主机名;
- 解析所有 A 和 AAAA 记录,禁止回环、私有、链路本地、保留和组播地址;
- 禁止或严格限制重定向,并对每次跳转重新检查;
- 限制响应大小、连接时间、读取时间和并发数;
- 将抓取服务部署到独立网络区域,通过防火墙或出口代理限制可访问网段;
- 不把远程响应原样返回给用户,尤其是可能包含凭据或内部错误信息的响应。
六、在授权环境中的安全验证方法
验证 SSRF 不需要访问真实内网资源。可以在本地或隔离测试网络中启动一个无敏感数据的 HTTP 服务,记录请求来源、Host、路径和请求头,然后让待测应用访问该测试服务。
python3 -m http.server 18080 --bind 127.0.0.1如果需要观察请求细节,可以使用专门的测试服务或在自有容器中编写简单的日志接口。验证重点包括:
- 请求是否确实由服务端发起,而不是浏览器直接发起;
- 服务端请求的源地址和网络出口是否符合预期;
- 应用是否跟随重定向;
- 响应内容是否会被原样返回;
- 超时、异常和非 2xx 响应是否会泄露内部信息。
测试报告中应记录入口参数、代码路径、请求时间、测试服务日志和修复建议。不要使用云元数据地址、生产管理端口或第三方站点作为证明材料,这些目标既没有必要,也容易造成越权访问。
七、代码审计中的常见误区
- 只看黑名单。 黑名单很难覆盖所有地址表示方式和解析差异,优先采用白名单。
- 只校验 URL 字符串。 应使用结构化解析结果判断协议、主机、端口和用户信息。
- 校验一次就结束。 DNS 解析、重定向和最终连接都可能改变目标,关键步骤需要重复约束。
- 忽视响应处理。 即使不能读取响应,SSRF 仍可能造成内部请求;如果响应可回显,风险通常更高。
- 只在应用层修复。 网络隔离、出口代理和最小权限同样重要,不能把全部安全性寄托在一段输入校验代码上。
- 没有资源限制。 SSRF 常与文件下载、连接耗尽和大响应导致的内存问题叠加,应设置大小、时间和并发限制。
八、修复后的回归测试清单
修复完成后,建议将以下场景加入自动化测试或安全回归测试:
| 测试项 | 预期结果 |
|---|---|
| 非允许协议 | 拒绝请求,不建立网络连接 |
| 未在白名单中的域名 | 拒绝请求 |
| 本机、私有和链路本地地址 | 拒绝请求 |
| IPv4 与 IPv6 特殊地址 | 均能正确识别并拒绝 |
| 带用户信息或异常端口的 URL | 拒绝请求 |
| 跳转到不允许目标 | 禁止跳转或在跳转前拒绝 |
| 超大响应和慢响应 | 触发大小或时间限制 |
| 解析失败、连接失败和证书错误 | 返回通用错误,不泄露内部细节 |
九、总结
SSRF 的核心不是某个特殊 Payload,而是服务端把外部可控数据当成网络目标使用。高质量的审计应围绕完整数据流展开:确认输入来源,定位请求 sink,分析 URL 解析与 DNS 行为,检查重定向和代理配置,再结合网络隔离判断实际影响。
修复时,优先采用明确的目的地白名单;如果业务必须访问任意外部站点,则应同时落实协议限制、地址过滤、重定向控制、超时和响应大小限制,以及独立网络出口。最后通过本地隔离服务完成验证,并将关键场景固化为回归测试,避免同类问题在其他抓取功能中重复出现。
推荐意见