SSRF(Server-Side Request Forgery,服务端请求伪造)并不只是“让服务器代替用户发起一次请求”。在真实系统中,它往往与网络分区、云环境元数据服务、内部管理接口以及权限模型相互叠加,最终形成从外部输入到内部资源的访问链路。对这类问题进行审计时,单纯搜索 requests.get() 或 HttpClient 并不够,更重要的是理解数据流、请求边界和部署环境。
一、问题背景与常见场景
SSRF 通常出现在以下功能中:
- 根据用户提交的 URL 抓取远程图片、网页或 RSS。
- Webhook、回调地址、第三方通知地址配置。
- 在线预览、文档转换、PDF 生成和远程资源解析。
- 服务端健康检查、连通性测试和网络探测功能。
- 代理下载、跳转跟随和文件导入功能。
这些功能本身并不一定有问题。风险主要取决于三个因素:用户是否能够影响目标地址,服务端是否会访问不应暴露的网络区域,以及返回内容是否会被直接回显或进一步处理。
审计时应把 SSRF 看作一条完整的数据流,而不是某一行危险函数。输入校验、DNS 解析、连接建立、重定向、响应读取和后续解析,每一个环节都可能改变最终风险。
二、先画出请求链路,再判断风险
一个典型的远程资源获取流程可以抽象为:
用户输入 URL
↓
参数校验与规范化
↓
DNS 解析
↓
建立 TCP/TLS 连接
↓
处理重定向
↓
读取响应
↓
解析或存储内容代码审计时建议沿着这条链路逐项确认:
- 输入是否允许完整 URL,还是只允许固定域名下的路径。
- 校验发生在 DNS 解析之前还是之后。
- 校验通过后是否重新解析 DNS。
- HTTP 客户端是否默认跟随重定向。
- 是否允许非 HTTP、HTTPS 协议。
- 是否限制响应大小、连接超时和读取超时。
- 响应是否会被当作 XML、图片、压缩包或模板继续处理。
如果只在入口处做字符串判断,例如判断 URL 是否包含某个域名,通常不足以证明安全。URL 解析存在用户名、端口、编码、IPv6、重定向和 DNS 变化等多个边界条件,字符串匹配很容易与实际连接目标不一致。
三、一个常见但不可靠的实现
下面的示例仅用于说明审计思路。它试图通过字符串判断阻止访问内部地址,但实际防护能力很弱:
public byte[] fetch(String input) throws IOException {
if (input.contains("127.0.0.1") || input.contains("localhost")) {
throw new IllegalArgumentException("blocked");
}
URL url = new URL(input);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
return conn.getInputStream().readAllBytes();
}这段代码至少存在以下问题:
- 通过 IP 的其他表达形式可能绕过文本判断。
- DNS 解析结果没有进行地址分类。
- 没有限制重定向,目标服务器可能返回另一个地址。
- 未限制响应体大小,容易造成内存或磁盘资源耗尽。
- 允许任意协议转换或 URL 解析行为,具体风险取决于客户端实现。
- 没有明确限制端口和出口网络范围。
四、正确的审计方法:验证“最终连接目标”
防护的核心不是判断用户输入长什么样,而是确认客户端最终连接到什么地址。建议把目标解析为结构化对象,并对主机、端口、协议和解析结果分别检查。
在 Java 服务中,可以将校验逻辑拆成以下几个阶段:
URI uri = URI.create(input);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("unsupported scheme");
}
if (host == null || host.isBlank()) {
throw new IllegalArgumentException("invalid host");
}
if (port != -1 && port != 80 && port != 443) {
throw new IllegalArgumentException("unsupported port");
}这只是基础校验,不能单独作为完整防护。之后还需要解析主机名,并对每个解析结果进行地址分类。对于业务不需要访问内网资源的服务,通常应拒绝回环地址、链路本地地址、私有地址、未指定地址以及其他保留地址。
InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()) {
throw new IllegalArgumentException("destination is not allowed");
}
}实际项目中不建议仅依赖这几个便捷方法。应结合组织的网络策略,对 IPv4、IPv6、云平台地址段、办公网地址段和容器网络地址段进行统一封装,并为该组件配套单元测试和回归测试。
五、DNS 重绑定与校验时机问题
一个经常被忽略的问题是:校验阶段解析到的地址,不一定是连接阶段使用的地址。若应用先解析域名并判断结果安全,随后由 HTTP 客户端再次解析域名,就存在两次解析结果不一致的可能。
因此,较稳妥的做法包括:
- 在受控网络环境中使用固定的出口代理,由代理统一执行最终连接和地址策略。
- 使用能够固定解析结果或提供自定义 DNS 解析器的 HTTP 客户端。
- 在建立连接前后都执行地址检查,并避免客户端自动重新解析未经校验的主机名。
- 通过网络层策略阻断应用容器访问不必要的内网网段,不能只依赖业务代码。
在审计报告中,建议明确写出“校验点”和“连接点”是否一致,而不是笼统描述为“存在 DNS 绕过”。这样更便于开发人员定位客户端配置和网络出口问题。
六、重定向是第二个高频风险点
即使初始 URL 通过了校验,服务端也可能返回 3xx 响应,引导客户端访问另一个地址。如果 HTTP 客户端默认跟随重定向,那么真正的访问目标已经发生变化。
安全处理方式有两种:
- 业务不需要重定向时,直接关闭自动跟随,并将 3xx 作为失败处理。
- 业务确实需要重定向时,对每一跳的目标重新解析、重新校验,并限制最大跳转次数。
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(3))
.build();对于下载器、图片代理等功能,建议把重定向次数限制在较小范围内,并限制每次跳转只能使用允许的协议和端口。不要仅校验初始地址。
七、响应内容同样属于攻击面
SSRF 的影响不只来自“能否访问某个地址”,还取决于应用如何处理响应。常见的二次风险包括:
- 读取内部接口返回的配置、调试信息或身份凭据。
- 将响应内容直接回显给前端,形成信息泄露。
- 把响应当作 XML 解析,引入 XXE 风险。
- 把响应当作图片、压缩包或文档处理,触发解析器漏洞。
- 无限读取大响应,导致内存、磁盘或线程资源耗尽。
因此,远程资源获取功能至少应设置连接超时、读取超时、最大响应大小、最大重定向次数和允许的 Content-Type。不要只根据响应头判断安全,服务端返回的头部本身也可能不可信,必要时应对实际内容做有限度的格式识别。
long maxBytes = 5 * 1024 * 1024;
try (InputStream in = response.body()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
long total = 0;
int n;
while ((n = in.read(buffer)) != -1) {
total += n;
if (total > maxBytes) {
throw new IOException("response too large");
}
out.write(buffer, 0, n);
}
}八、在隔离环境中进行安全验证
验证 SSRF 时不应直接探测生产内网,也不应访问真实的管理接口或云平台元数据服务。更推荐建立最小化实验环境:
- 使用本地容器启动一个业务服务和一个仅用于记录请求的测试服务。
- 让测试服务返回固定的非敏感标记,例如
TEST-SSRF-OK。 - 通过应用日志、容器网络日志和代理日志确认实际请求目标。
- 对重定向、超时、超大响应和不同地址类型分别编写自动化测试。
例如,可以在本地测试服务中记录请求来源、Host、路径和 User-Agent,再由业务服务访问该测试地址。测试的重点是确认请求是否按照预期被限制,而不是尝试读取任何真实敏感信息。
curl --max-time 3 SSRF,合理的身份权限也能降低进一步影响。十、代码审计中的排查清单
- 搜索所有由用户输入决定目标地址的代码路径,而不只是搜索 HTTP 客户端调用。
- 检查 URL 是否经过结构化解析,避免纯字符串黑名单。
- 确认是否校验协议、主机、端口和解析后的全部地址。
- 检查 IPv4、IPv6、域名多解析结果以及重定向处理。
- 确认 DNS 校验与实际连接是否使用同一套解析结果。
- 检查连接、读取、响应大小和重定向次数限制。
- 确认响应是否进入 XML、图片、压缩包、模板或脚本解析流程。
- 检查日志中是否包含完整 URL、Authorization 头或敏感响应内容。
- 结合容器、主机、防火墙和云网络策略判断真实可达范围。
- 为修复方案补充回归测试,避免后续重构重新引入问题。
十一、修复后的回归测试设计
回归测试不应只验证一个被拦截的地址,而应覆盖整个请求状态机。可以至少准备以下测试类别:
- 合法的业务域名和合法端口能够正常访问。
- 回环、私有、链路本地和未指定地址被拒绝。
- 主机名解析得到多个地址时,只要存在不允许地址就拒绝。
- 初始地址合法、重定向目标不合法时,请求被终止。
- 响应超过大小限制时,连接被安全关闭。
- 连接超时和读取超时不会长期占用工作线程。
- 不支持的协议和端口无法进入底层客户端。
- 错误日志包含足够的排查信息,但不泄露敏感响应内容。
总结
SSRF 审计的关键不是寻找某个“万能黑名单”,而是建立清晰的请求边界:谁提供目标地址,应用解析出了什么,最终连接到了哪里,响应又被如何处理。比较可靠的治理方式通常由四部分组成:结构化解析与严格校验、对 DNS 和重定向保持一致控制、限制响应处理资源,以及通过代理和网络隔离补充应用层防护。
从工程实践看,最值得优先改进的往往不是某一行过滤代码,而是把远程请求能力集中到一个经过审计的组件中,统一处理协议、地址、端口、超时、大小和日志策略。这样既能降低修复成本,也能避免不同业务各自实现一套不完整的 SSRF 防护逻辑。

服务器、云资产与权限边界梳理示意
推荐意见