服务端请求伪造(SSRF)并不只是“接口能不能访问内网”的问题。实际审计中,漏洞往往出现在 URL 解析、DNS 解析、重定向处理和出站网络策略之间的不一致:应用认为目标是公网域名,底层网络却最终连接到了本机、内网服务或云环境管理接口。
这类问题的修复也不能依赖一个简单的字符串黑名单。本文以代码审计和安全验证为主线,整理 SSRF 的常见成因、审计路径、修复思路和测试重点。示例均应在本地靶场或授权测试环境中使用,不涉及访问真实内部系统。
一、典型业务场景
以下功能最容易引入 SSRF:
- 通过 URL 抓取图片、文档或网页内容;
- Webhook、回调地址和第三方通知配置;
- 远程导入、在线预览、网页截图和 PDF 转换;
- 根据用户提供的地址检查站点状态;
- 解析 RSS、OpenAPI 文档或远程配置文件。
一个常见的危险实现如下:
String target = request.getParameter("url");
URL url = new URL(target);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
return conn.getInputStream().readAllBytes();这里没有限制协议、目标地址、端口、响应大小和重定向行为。即使增加了 startsWith("http") 之类的检查,也不能解决核心问题,因为 URL 的语义必须经过规范化解析,不能用字符串匹配代替。
二、审计时先确认“请求链”
审计不要只盯着控制器。应沿着用户输入追踪到真正建立网络连接的位置,重点确认以下问题:
- 输入是否进入了 HTTP 客户端、DNS 客户端、文件读取 API 或第三方抓取组件;
- 是否存在二次解析,例如先把 URL 交给代理,再由代理重新解析域名;
- 是否启用了自动重定向;
- 域名解析发生在应用、代理还是容器网络层;
- 响应是否会被完整读入内存、落盘或再次交给解析器;
- 失败信息、响应头和响应体是否会回显给用户。
在 Java 项目中,可以重点搜索 URL、URI、HttpClient、HttpURLConnection、RestTemplate、WebClient、OkHttp 以及自定义的下载工具类。Python 项目则可关注 requests、urllib、aiohttp 和基于浏览器的抓取组件。
三、最容易被忽略的解析问题
1. 协议限制不完整
业务通常只需要 HTTP 和 HTTPS,却可能意外支持 file、ftp、gopher 或其他由底层库处理的协议。修复时应采用正向允许列表,而不是维护不断增长的危险协议黑名单。
if (!scheme.equals("http") && !scheme.equals("https")) {
throw new IllegalArgumentException("unsupported scheme");
}2. 主机名和端口判断不一致
字符串中出现一个允许域名,并不代表最终连接目标就是该域名。审计时要关注用户名信息、端口、IPv6 表示、尾随点、大小写和编码字符等情况。主机名应使用标准 URI 解析器取得,不能通过截取字符串判断域名后缀。
例如,判断是否属于 example.com 时,不能只检查是否以该字符串结尾。正确的边界应是主机名等于该域名,或以 .example.com 作为完整后缀,否则类似 example.com.attacker.test 的主机名可能被错误放行。
3. DNS 解析和实际连接之间的竞态
一种常见的修复方式是先解析域名,确认返回地址不是内网地址,再交给 HTTP 客户端重新连接。问题在于,校验和连接可能使用了两次不同的 DNS 结果,形成检查时与使用时不一致。
更稳妥的设计是让出站代理或网络策略完成最终连接控制,应用侧只负责业务级允许列表。若必须在应用中实现,应尽量复用已校验的解析结果,禁用自动代理和不受控的重解析,并对每次重定向重新执行完整校验。
4. 重定向绕过初始校验
即使首次 URL 是公开站点,也可能返回指向内网地址的 30x 响应。因此 SSRF 防护不能只检查首次请求。建议默认关闭自动重定向;确需支持时,应限制跳转次数,并对每个新地址重新检查协议、主机、端口和解析结果。
四、推荐的防护分层
第一层:业务允许列表
如果业务目标固定,例如只允许访问几个合作方,应保存经过审核的域名或资源标识,避免让用户直接提交任意 URL。域名、端口、路径和证书校验都应纳入规则。
第二层:规范化后再校验
流程顺序建议如下:
- 解析 URI,拒绝缺少主机名、异常编码和不支持的协议;
- 规范化主机名和端口,明确默认端口的处理方式;
- 解析所有地址记录,拒绝回环、链路本地、私有、保留和未分配地址;
- 限制端口范围,除非业务确实需要,否则只允许 80 和 443;
- 关闭或严格控制重定向;
- 限制连接超时、读取超时、响应大小和总下载时间。
地址分类不能只判断 IPv4。生产环境还应覆盖 IPv6 的回环地址、链路本地地址、唯一本地地址以及 IPv4 映射地址。下面是防护逻辑的示意代码,具体实现应使用经过验证的网络地址库:
URI uri = URI.create(input);
String scheme = uri.getScheme();
if (!Set.of("http", "https").contains(scheme)) {
throw new SecurityException("scheme denied");
}
String host = IDN.toASCII(uri.getHost()).toLowerCase(Locale.ROOT);
int port = uri.getPort() == -1 ? defaultPort(scheme) : uri.getPort();
if (!allowedPort(port) || host.isBlank()) {
throw new SecurityException("host or port denied");
}
for (InetAddress address : resolver.resolve(host)) {
if (isLoopback(address) || isPrivate(address)
|| isLinkLocal(address) || isReserved(address)) {
throw new SecurityException("non-public address denied");
}
}这段代码只表达控制点,不应直接复制到生产环境。尤其是 isPrivate 和 isReserved 的实现,要确认依赖库对 IPv4、IPv6 和映射地址的覆盖范围。
第三层:网络出口控制
应用层校验容易受到 DNS、代理和组件行为差异影响。更可靠的做法是在容器、主机或出站代理层限制服务访问范围:
- 抓取服务使用独立网络区域,默认拒绝访问内网网段;
- 通过防火墙或安全组限制出站端口;
- 禁止访问云平台管理地址和本机管理接口;
- 使用专用 DNS 解析策略,避免应用直接访问不必要的内部解析服务;
- 在代理层记录目标域名、解析地址、最终连接地址和重定向链。
五、如何在授权环境中验证修复
建议搭建两个本地服务:一个模拟业务允许访问的公网目标,另一个模拟不应被访问的内部服务。测试重点不是读取敏感内容,而是验证请求是否被策略拒绝,并检查日志是否记录了原因。
# 启动本地测试服务,仅用于本机靶场
python3 -m http.server 18080 --bind 127.0.0.1测试用例至少应覆盖:
- 正常的 HTTP 和 HTTPS 地址;
- 不支持的协议;
- 无主机名、非法端口和异常 URL 编码;
- 解析到回环、私有、链路本地和 IPv6 本地地址的域名;
- 返回重定向的地址;
- 多次解析结果不同的域名;
- 超大响应、慢响应和连接失败场景。
验证结果应同时观察应用日志、代理日志和网络设备日志。只看 HTTP 状态码不够,因为某些客户端在异常时仍可能建立了 TCP 连接。
六、日志与监控中的关键字段
出站请求建议记录请求来源、业务接口、原始 URL 的脱敏形式、规范化主机名、目标端口、解析到的地址、是否发生重定向、拒绝原因和耗时。不要把完整响应体或可能包含凭据的 URL 直接写入日志。
监控上可以关注以下异常:
- 同一接口短时间访问大量不同域名;
- 请求集中出现在非业务端口;
- 大量 DNS 失败、连接超时或重定向;
- 抓取服务访问地址空间异常分散;
- 应用进程出现不符合业务的长连接和出站流量。
七、审计中的常见误区
- 只做黑名单。 黑名单难以覆盖所有地址表示和协议变体,应该优先采用允许列表。
- 只校验域名字符串。 必须关注解析后的地址以及最终连接目标。
- 只校验第一次请求。 重定向、代理和二次解析都会改变目标。
- 只依赖应用代码。 出站网络隔离是重要的第二道防线。
- 为了方便开启自动跟随重定向。 这会让校验边界变得模糊,应默认关闭。
- 忽略响应处理。 大响应、压缩炸弹和恶意内容解析同样可能造成拒绝服务或二次漏洞。
总结
SSRF 的本质是“用户可控输入影响了服务端的出站连接”,真正困难的部分在于 URL 解析、DNS、重定向、代理和网络层之间的边界。代码审计时,应从输入点追踪到实际连接点;修复时,应采用协议和目标的正向允许列表、对每次跳转重新校验,并通过出站网络策略限制应用能力。
如果业务允许,最有效的方案通常不是让主应用直接访问任意 URL,而是将抓取能力拆分为独立服务,配合严格的出口代理、资源限制和审计日志。这样即使某一层校验出现遗漏,也能把风险控制在可观测、可阻断的范围内。
推荐意见