SSRF 是 Web 安全里很常见的一类问题,但在实际代码审计中,它往往不是以“明显漏洞点”的形式出现,而是藏在图片抓取、Webhook 回调、URL 预览、文件导入、第三方接口转发等业务逻辑里。本文整理一次 Spring 应用中的 SSRF 风险排查思路,重点放在如何识别风险、如何验证边界、以及如何做工程化加固。
文章内容仅讨论授权环境下的安全测试和防护实现,不涉及对第三方系统的攻击利用。
背景
某业务系统提供了一个“远程资源导入”功能,用户提交一个 URL,后端会下载该 URL 对应的图片或文档,并保存到对象存储。功能本身很常见,代码逻辑大致如下:
String url = request.getParameter("url");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<byte[]> resp = restTemplate.getForEntity(url, byte[].class);
byte[] data = resp.getBody();
// 后续进行类型识别、上传对象存储从业务视角看,这段代码只是“下载用户指定的资源”。但从安全视角看,只要服务端能够访问内网、云元数据地址、管理面服务或其他不应由用户间接访问的地址,就存在 SSRF 风险。
典型风险场景
在审计这类功能时,我通常会先把入口点按业务归类,而不是只搜索某个危险 API。常见场景包括:
- 头像、封面、附件的远程 URL 导入。
- 文章链接预览、Open Graph 信息抓取。
- Webhook、Callback、通知地址配置。
- PDF/HTML 转换服务读取远程资源。
- 图片代理、接口代理、第三方数据同步。
- XML、YAML、Markdown 渲染过程中的外部资源加载。
这类入口的共同点是:用户可控输入最终影响了服务端发起的网络请求。
问题代码分析
以 Spring 项目为例,常见的网络请求 API 包括 RestTemplate、WebClient、HttpClient、OkHttp、URLConnection 等。审计时可先检索这些调用点,再反向追踪参数来源。
grep -R "RestTemplate\|WebClient\|HttpClient\|OkHttpClient\|openConnection" -n src/main/java如果发现 URL 参数来自 HTTP 请求、数据库配置、消息队列或外部接口返回值,都应继续确认是否存在严格校验。下面是一个常见的错误做法:
if (!url.startsWith("http")) {
throw new IllegalArgumentException("invalid url");
}
return restTemplate.getForObject(url, byte[].class);这类判断基本没有安全意义。攻击面不只在协议前缀,还包括主机名解析、重定向、IPv6、内网地址段、DNS 变更、特殊格式 IP、URL 解析差异等。
验证思路:先确认“能力边界”
SSRF 验证不建议一上来就尝试敏感地址。更稳妥的做法是先搭建一个可控的回显服务,确认服务端是否会对外发起请求,以及请求方法、Header、User-Agent、超时行为等信息。
# 在授权测试环境中启动一个简单 HTTP 服务
python3 -m http.server 8000
# 或使用 nc 观察原始请求
nc -lv 8000提交测试 URL 后,如果回显服务收到来自后端服务器的请求,说明“用户输入影响服务端请求”这一点成立。接下来再验证防护边界,例如是否允许重定向、是否限制 Content-Type、是否限制响应大小、是否限制下载耗时等。
实际项目中,SSRF 的危害不只取决于代码本身,也取决于服务部署环境。如果后端位于云环境、K8s 集群、办公内网或能访问管理接口,风险等级会明显上升。
容易忽略的绕过点
防护 SSRF 时,很多团队会简单做域名黑名单或字符串匹配,但这通常不够。下面这些点在代码审计中比较常见:
- 重定向:初始 URL 是合法域名,但 302 跳转到不允许访问的地址。
- DNS 解析变化:校验时解析到公网地址,请求时解析到内网地址。
- IPv6 与 IPv4 映射:只判断 IPv4 私网段,忽略 IPv6 本地地址和映射地址。
- 特殊 IP 表示:十进制、八进制、十六进制或混合格式导致判断失效。
- URL 解析差异:业务校验使用一种解析器,请求库使用另一种解析器。
- 协议扩展:除 http/https 外,某些库可能支持 file、ftp、gopher 等协议。
- 响应过大:虽然不是 SSRF 本身,但可能导致内存或磁盘资源被耗尽。
加固原则
我个人更推荐“白名单 + 解析后校验 + 请求过程约束”的组合,而不是依赖黑名单。核心原则如下:
- 只允许 http 和 https 协议。
- 业务允许的情况下,限制目标域名白名单。
- 在发起请求前解析主机名,校验解析出的所有 IP。
- 禁止访问本地地址、私有地址、链路本地地址、保留地址。
- 禁用自动重定向,或每次重定向后重新校验目标地址。
- 设置连接超时、读取超时、最大响应大小。
- 限制 Content-Type,只处理业务需要的类型。
- 请求组件使用独立网络出口策略,必要时通过网关做 egress 控制。
Java 中的基础校验示例
下面示例只展示基础思路,生产环境还需要结合业务白名单、日志审计、网关策略一起做。
public final class UrlSecurityChecker {
public static void check(String rawUrl) throws Exception {
URI uri = new URI(rawUrl).normalize();
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("unsupported scheme");
}
String host = uri.getHost();
if (host == null || host.isBlank()) {
throw new IllegalArgumentException("empty host");
}
InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
if (isUnsafeAddress(address)) {
throw new IllegalArgumentException("unsafe target address: " + address.getHostAddress());
}
}
}
private static boolean isUnsafeAddress(InetAddress address) {
return address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| address.isMulticastAddress();
}
}需要注意的是,isSiteLocalAddress() 可以覆盖常见私网地址,但对云环境、保留地址、特殊地址段的覆盖并不完整。更稳妥的做法是维护一组明确的禁止网段,并使用成熟的 IP/CIDR 判断库。
RestTemplate 请求约束示例
在请求层面,应设置超时并控制重定向。以下示例用于说明配置方向:
@Bean
public RestTemplate safeRestTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
return new RestTemplate(factory);
}如果使用 Apache HttpClient,可以进一步配置重定向策略,避免请求库自动跟随跳转:
CloseableHttpClient httpClient = HttpClients.custom()
.disableRedirectHandling()
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);如果业务确实需要支持重定向,建议最多允许有限次数,并对每一次 Location 指向的 URL 重新执行完整校验。
响应处理也要做限制
很多 SSRF 修复只关注“能不能访问”,忽略了“访问后如何处理”。如果后端直接把响应读入内存,可能被大文件拖垮。建议在读取阶段限制大小:
private static byte[] readWithLimit(InputStream input, int maxBytes) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int total = 0;
int len;
while ((len = input.read(buffer)) != -1) {
total += len;
if (total > maxBytes) {
throw new IOException("response too large");
}
output.write(buffer, 0, len);
}
return output.toByteArray();
}对于图片导入类功能,还应在保存前进行真实文件类型识别,不要只依赖 URL 后缀或 Content-Type。
网络侧防护
代码层加固是必要的,但不应是唯一防线。比较稳的做法是在运行环境中增加出口访问控制:
- 容器或主机层禁止访问元数据地址、管理网段、数据库网段。
- K8s 环境使用 NetworkPolicy 限制 Pod 出口。
- 通过统一代理或网关转发外部请求,并在网关层做域名与 IP 校验。
- 对高风险功能使用独立运行时身份,减少其内网访问权限。
例如 K8s 中可以通过 NetworkPolicy 对特定命名空间做默认拒绝,再按需放行外部代理服务。具体策略要结合 CNI 插件能力和业务拓扑,不建议直接照搬。
日志与监控建议
SSRF 类问题修复后,日志也很关键。建议记录以下信息,便于回溯和告警:
- 用户提交的原始 URL。
- 规范化后的 URL。
- 解析出的目标 IP 列表。
- 最终请求的 URL,包含重定向后的地址。
- 请求耗时、响应大小、Content-Type。
- 被拒绝原因,例如私网地址、协议不允许、响应过大。
日志中要避免记录敏感 Header、Token、Cookie 等内容。对于被拒绝访问内网地址的行为,可以单独打安全事件日志,便于后续分析是否存在探测行为。
代码审计检查清单
| 检查项 | 建议 |
|---|---|
| URL 来源 | 确认是否来自用户输入、外部接口、消息队列或配置中心 |
| 协议限制 | 只允许 http/https,拒绝其他协议 |
| 主机校验 | 优先使用白名单,避免仅做字符串黑名单 |
| IP 校验 | 解析所有 IP,拒绝本地、私网、保留地址 |
| 重定向 | 禁用自动重定向,或每次跳转后重新校验 |
| 超时限制 | 设置连接超时和读取超时 |
| 响应大小 | 限制最大下载大小,避免资源消耗 |
| 内容类型 | 按业务限制 Content-Type,并做真实类型识别 |
| 网络出口 | 通过防火墙、NetworkPolicy、代理网关做二次限制 |
| 审计日志 | 记录关键字段,便于追踪异常请求 |
总结
SSRF 的难点不在于某个 API 是否危险,而在于业务功能是否允许用户间接控制服务端网络请求。单纯判断 URL 字符串通常不可靠,比较稳妥的做法是从业务入口、URL 解析、DNS 解析、IP 校验、重定向处理、响应限制和网络出口多个层面一起收敛风险。
在实际修复中,如果业务只需要访问固定第三方服务,域名白名单是最直接有效的方案;如果必须支持用户提交任意公网 URL,则一定要把解析后 IP 校验、重定向复检、超时和响应大小限制补齐。最后再配合网络侧出口控制,才能避免代码层漏判带来的风险扩大。
推荐意见