SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可以访问 URL 的参数”这么简单。真正有价值的审计,需要回答三个问题:请求是否由服务端发起、攻击者能控制请求的哪些部分、服务端出站网络能够到达什么位置。
在实际项目中,SSRF 常出现在图片抓取、Webhook、在线预览、文件导入、PDF 转换、第三方接口代理等功能里。它的风险也取决于网络边界和权限配置:一个只能访问公网的请求代理,影响可能有限;如果能够触达内部管理接口、云环境元数据服务或未鉴权的运维组件,风险就会明显扩大。
一、审计场景与基本判断
常见入口包括以下几类:
- 根据 URL 下载图片、头像或远程附件。
- 将 Markdown、HTML 或文档转换为 PDF。
- 向用户指定的 Webhook 地址发送通知。
- 通过 URL 抓取网页标题、生成缩略图或进行内容预览。
- 后台提供的“连通性检测”“接口测试”“导入远程配置”等功能。
审计时不要只搜索参数名 url。更可靠的方法是从网络请求 API 反向追踪数据流,重点关注 Java 的 HttpClient、URLConnection、OkHttp,Go 的 http.Client,Python 的 requests、urllib,以及 Node.js 中的 fetch、axios 等调用点。
判断 SSRF 的核心不是“是否使用了 HTTP 客户端”,而是“外部可控数据是否能影响请求目标,并且缺少足够的目标校验和出站限制”。
二、从数据流定位真正的请求点
以一个简化的 Java 服务为例,控制器接收用户提交的 URL,随后交给业务层下载内容:
@PostMapping("/preview")
public PreviewResult preview(@RequestParam String target) {
return previewService.fetch(target);
}
public byte[] fetch(String target) {
try {
URI uri = URI.create(target);
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5))
.GET()
.build();
return httpClient.send(request,
HttpResponse.BodyHandlers.ofByteArray()).body();
} catch (Exception e) {
throw new IllegalArgumentException("fetch failed", e);
}
}这个例子中,风险点至少有四个:
target完全来自请求参数。- 没有限制协议,URI 解析结果可能包含非预期 scheme。
- 没有校验目标 IP 是否属于内网、回环、链路本地或保留地址。
- 没有说明重定向策略、响应大小、响应类型和连接数量。
实际审计时还要继续追踪 httpClient 的构造位置。即使业务代码没有显式处理重定向,底层客户端也可能默认跟随;即使第一轮校验通过,重定向后的第二个地址仍可能落入禁止网段。
三、URL 校验中最容易遗漏的边界
1. 协议与端口
一般只允许业务确实需要的协议,例如 http 和 https。不要把协议校验建立在字符串前缀上,例如判断是否以 开头。应当使用标准 URI 解析器读取 scheme、host、port,并拒绝空主机、异常端口和不必要的协议。
2. 主机名解析与 IP 校验
仅检查用户输入中是否包含 127.0.0.1、localhost 等字符串是不够的。攻击者可以使用域名、IPv6、不同进制表示或 DNS 解析结果变化来绕过简单规则。
较稳妥的流程是:
- 先解析 URI,确认协议、主机名和端口符合业务要求。
- 对主机名进行 DNS 解析。
- 检查所有解析结果,而不是只检查第一个地址。
- 拒绝回环、私有、链路本地、组播、未分配和其他保留地址。
- 建立连接前再次确认目标地址,避免校验与连接之间存在时间窗口。
在 Java 中可以将地址分类判断封装成独立组件,避免散落在多个控制器里:
boolean isBlocked(InetAddress address) {
return address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| isReservedRange(address);
}isSiteLocalAddress() 不能覆盖所有需要禁止的范围,生产环境应根据 IPv4、IPv6 和云网络规划补充明确的 CIDR 列表,并通过单元测试覆盖边界地址。
3. DNS 重绑定问题
如果校验阶段解析到公网地址,而真正建立连接时再次解析得到内部地址,就可能出现 DNS 重绑定。应尽量让校验结果与实际连接使用同一个解析结果,或者在受控网络代理中统一完成解析和连接。对于高风险场景,建议不允许服务端直接解析用户提供的任意域名,而是采用固定出口代理和域名白名单。
4. 重定向
重定向是审计中经常被忽略的一层。第一跳地址合法,不代表 Location 指向的下一跳也合法。修复方案通常有两种:
- 业务不需要重定向时直接关闭自动跟随。
- 必须支持重定向时,每一跳都重新执行协议、域名、解析结果和端口校验,并限制最大跳数。
四、构造安全的请求流程
一个相对完整的安全请求流程应当同时控制目标、协议、连接和响应。下面是逻辑示例,重点在流程而非直接复制到生产环境:
URI uri = parseAndValidate(input);
if (!Set.of("http", "https").contains(uri.getScheme().toLowerCase())) {
throw new SecurityException("unsupported scheme");
}
List addresses = resolver.resolve(uri.getHost());
if (addresses.isEmpty() || addresses.stream().anyMatch(this::isBlocked)) {
throw new SecurityException("blocked destination");
}
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5))
.header("Accept", "image/*")
.GET()
.build();
HttpResponse<InputStream> response = client.send(
request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() / 100 == 3) {
throw new SecurityException("redirect is not allowed");
}
if (!isAllowedContentType(response.headers().firstValue("Content-Type"))) {
throw new SecurityException("unexpected content type");
}
byte[] body = readAtMost(response.body(), 5 * 1024 * 1024); 这里仍然需要注意连接实际使用的地址是否与校验地址一致。不同 HTTP 客户端的 DNS、连接池、代理和 IPv6 行为可能不同,不能只看业务层代码就认为校验已经生效。
五、通过网络层降低 SSRF 影响
应用层校验不是唯一防线。更可靠的设计是让应用运行在默认拒绝出站访问的网络环境中,只开放业务必需的目标。
- 在容器、主机防火墙或云安全组中限制出站网段和端口。
- 将远程抓取功能放入隔离的任务服务,不与核心业务共享高权限网络身份。
- 通过固定的出站代理访问公网,并在代理层做域名、端口和响应大小控制。
- 避免让业务实例直接接触云平台元数据服务;在云环境中使用平台提供的元数据访问保护机制。
- 对 DNS 请求、HTTP 出站、异常端口连接建立审计日志和告警。
需要注意的是,网络隔离不能替代应用层修复。应用仍应限制 URL、重定向、响应体和请求时间,因为 SSRF 还可能被用于访问内部但本身不属于高敏感网段的服务,或者消耗连接和带宽形成拒绝服务。
六、如何在授权环境中验证修复
验证应在本地测试环境或明确授权的测试网络中进行,不要直接探测生产内网。可以搭建两个本地服务:
- 服务 A 模拟业务接口,接收并处理用户提交的 URL。
- 服务 B 模拟允许访问的资源,同时记录请求头、路径和重定向行为。
首先验证正常业务:
curl -X POST \
--data-urlencode 'target= Content-Type 等情况,确认应用能够按预期拒绝,并且不会出现长时间占用线程、内存持续增长或连接池耗尽。建议将以下测试纳入自动化用例:
测试类别 验证内容 预期结果 协议 非 HTTP/HTTPS scheme 拒绝请求 地址 回环、私有、链路本地和 IPv6 保留地址 拒绝请求 解析 一个域名返回多个地址 所有地址均通过检查,否则拒绝 重定向 合法地址跳转到禁止地址 拒绝或不跟随 响应 超大内容、错误类型、慢速传输 受大小、类型和时间限制 异常 DNS 失败、连接超时、证书错误 安全失败且日志可追踪
七、日志与告警应该记录什么
SSRF 防护失败后,如果日志只记录“fetch failed”,后续很难定位问题。建议记录结构化字段:
- 业务功能和请求 trace ID。
- 经过规范化处理后的 scheme、域名和端口。
- 解析得到的地址类别,但避免记录不必要的敏感响应内容。
- 拒绝原因,例如协议不允许、目标地址被禁止、重定向超限或响应过大。
- 连接耗时、响应状态码和响应大小。
日志中的用户输入应进行长度限制和转义,避免攻击者利用 URL 或错误信息制造日志注入。对于短时间内大量访问不同域名、频繁命中禁止地址或持续触发超时的行为,可以结合账号、租户和来源网络进行限速和告警。
八、代码审计中的常见误区
- 只做字符串黑名单:黑名单很容易遗漏等价表示、IPv6 和 DNS 场景,应优先采用解析后的地址分类与白名单策略。
- 只校验第一次请求:重定向、代理和连接池可能让后续连接落到不同目标,必须覆盖完整请求链路。
- 只检查域名后缀:例如允许某个后缀并不能自动证明其解析地址安全,也要防止相似域名和解析漂移。
- 只关注数据泄露:SSRF 同样可能造成内网服务探测、异常请求触发和资源消耗。
- 把超时当成完整防护:超时只能降低部分资源消耗,不能替代目标校验、网络隔离和响应大小限制。
总结
SSRF 审计的重点是围绕“输入—解析—连接—重定向—响应—网络出口”建立完整的数据流视图。单独增加一个 URL 黑名单,通常只能挡住少量明显样本,无法应对解析差异、重定向、DNS 变化和多协议客户端行为。
更可执行的修复组合是:只允许业务需要的协议和端口;对解析后的所有地址执行统一的网段校验;谨慎处理 DNS 和重定向;限制连接时间、响应大小和内容类型;在网络层实施默认拒绝的出站策略;最后用本地授权环境完成回归测试和日志验证。这样才能把 SSRF 从一个模糊的风险点,落实为可审计、可测试、可持续维护的安全控制。

代码审计、调用链与关键函数定位示意
推荐意见