跳转到帖子

背景

SSRF(Server-Side Request Forgery,服务端请求伪造)在真实业务里并不少见,尤其常出现在“服务端代请求”的功能中,比如图片抓取、URL 预览、Webhook 回调测试、文件远程导入、接口连通性检测等。

这类问题的麻烦点在于:表面上只是服务端访问一个用户传入的 URL,但如果缺少边界控制,攻击面会从公网 URL 扩展到内网地址、云元数据服务、管理端口,甚至变成横向探测入口。本文不讨论攻击利用链,只从代码审计和修复角度整理一套可复用的方法。

典型场景

在一次 Java Web 项目审计中,看到类似功能:用户提交一个图片 URL,后端下载后生成缩略图并缓存。代码简化后如下:

public byte[] fetchImage(String url) throws IOException {
    URL target = new URL(url);
    HttpURLConnection conn = (HttpURLConnection) target.openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    conn.setRequestMethod("GET");

    try (InputStream in = conn.getInputStream()) {
        return in.readAllBytes();
    }
}

这段代码看起来很常见,也设置了超时,但关键问题是:用户可控的 URL 被服务端直接请求,且没有对协议、域名、解析结果、重定向、响应大小等做限制。

审计关注点

审计 SSRF 相关代码时,我一般会先从“入口”和“出口”两个方向找。

1. 常见入口

  • 图片、头像、附件的远程 URL 导入。
  • 文章链接预览、OpenGraph 信息抓取。
  • Webhook、回调地址、通知地址测试。
  • 后台配置里的第三方接口地址。
  • PDF/HTML 渲染服务中加载远程资源。
  • XML、YAML、模板引擎中可能触发外部资源加载的位置。

2. 常见危险写法

  • 直接使用 URL、URI、RestTemplate、HttpClient 请求用户输入。
  • 只判断字符串前缀,例如 startsWith("http")。
  • 只做黑名单过滤,例如过滤 127.0.0.1、localhost,但没有处理 IPv6、十进制 IP、重定向、DNS 变化。
  • 允许自动跟随重定向,但重定向后的地址未重新校验。
  • 没有限制响应大小,可能导致内存占用异常。
  • 没有限制 Content-Type,下载逻辑被用于访问非预期资源。

技术分析

SSRF 的本质不是“能不能访问某个地址”,而是“服务端是否替用户发起了超出业务边界的请求”。因此判断风险时,需要关注服务端请求发生在哪个网络位置、具备什么访问权限,以及业务是否必须支持任意地址。

下面是几个容易被忽略的点。

协议限制不足

很多代码只判断 URL 是否包含 http 字符串,这种判断没有意义。应该明确只允许 http 和 https,并拒绝 file、ftp、gopher、jar 等非业务协议。即使某些 Java HTTP 客户端不支持特定协议,也不应该依赖客户端默认行为作为安全边界。

URI uri = URI.create(inputUrl);
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
    throw new IllegalArgumentException("unsupported scheme");
}

域名解析与内网地址判断

只校验字符串中的 host 不够,必须解析 DNS 后检查 IP 地址是否落在不允许访问的网段。常见需要阻断的范围包括:

  • localhost、127.0.0.0/8。
  • 私有地址:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。
  • 链路本地地址:169.254.0.0/16。
  • IPv6 本地地址、链路本地地址、唯一本地地址。
  • 云环境中的元数据地址,例如常见链路本地元数据服务地址。

Java 中可以结合 InetAddress 做基础判断,但生产环境建议封装成统一组件,不要散落在业务代码里。

private boolean isBlockedAddress(InetAddress addr) {
    return addr.isAnyLocalAddress()
        || addr.isLoopbackAddress()
        || addr.isLinkLocalAddress()
        || addr.isSiteLocalAddress()
        || addr.isMulticastAddress();
}

需要注意的是,InetAddress 的内置判断不一定覆盖所有组织内部定义的敏感网段,例如办公网、Kubernetes Service 网段、容器网段、数据库网段等。这些应该通过配置补充。

重定向必须重新校验

一个常见绕过点是:初始 URL 指向允许的公网域名,但该地址返回 30x 跳转到不允许访问的地址。如果 HTTP 客户端自动跟随重定向,而代码只校验了第一次 URL,就会失效。

建议关闭自动重定向,手动处理 Location,并对每一次跳转后的 URL 重复做协议、域名、解析结果校验,同时限制跳转次数。

HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);

int status = conn.getResponseCode();
if (status >= 300 && status < 400) {
    String location = conn.getHeaderField("Location");
    // 对 location 解析成绝对 URL 后重新执行完整校验
}

DNS Rebinding 风险

DNS Rebinding 是 SSRF 防护中比较容易漏掉的情况:校验时解析到公网 IP,请求时却解析到内网 IP。降低风险的做法包括:

  • 解析后绑定到校验过的 IP,不在请求阶段重新解析未校验域名。
  • 使用内部统一的安全 HTTP 客户端,集中处理 DNS 与连接逻辑。
  • 对高风险功能使用白名单域名,而不是允许任意公网域名。
  • 在网络层增加 egress 控制,禁止业务服务访问不必要的内网网段。

仅靠应用层判断很难覆盖所有边界,网络层出站访问控制是很重要的兜底。

响应内容限制

SSRF 修复不只是不访问内网,还要避免资源消耗问题。建议至少限制:

  • 最大响应大小。
  • 允许的 Content-Type。
  • 连接超时和读取超时。
  • 最大重定向次数。
  • 下载后的文件类型校验。

例如图片抓取功能,不应该只看 URL 后缀,而应检查响应头和实际文件格式。

String contentType = conn.getContentType();
if (contentType == null || !contentType.toLowerCase().startsWith("image/")) {
    throw new IllegalArgumentException("invalid content type");
}

推荐修复思路

更稳妥的修复不是在每个业务方法里补几行判断,而是沉淀一个“安全 URL 请求组件”。业务方只传入 URL 和业务类型,安全组件统一完成校验、请求、重定向处理和大小限制。

1. 优先使用白名单

如果业务只需要访问固定合作方接口,直接配置域名白名单是最可靠的方案。白名单可以支持精确域名或受控的子域名规则,但要避免简单的字符串包含判断。

allowed.example.com        // 允许
api.partner.example        // 允许
evil-allowed.example.com   // 不应因包含 allowed 而被允许

2. 任意公网 URL 场景要做完整校验

如果业务确实需要抓取任意公网图片或页面,建议最少满足以下规则:

  • 只允许 http、https。
  • 禁止访问内网、回环、链路本地、多播地址。
  • 每次重定向后重新校验。
  • 限制响应大小和下载时间。
  • 限制 Content-Type。
  • 记录审计日志,包括原始 URL、最终 URL、解析 IP、响应状态、大小。

3. 网络层做出站限制

应用层修复可以减少漏洞入口,但不能完全替代网络层隔离。建议在容器、主机或网关层面限制服务的出站访问范围。比如图片抓取服务通常不需要访问数据库网段、管理网段、Kubernetes API、云元数据地址。

在 Linux 环境中,可以通过防火墙、Service Mesh、云安全组、Kubernetes NetworkPolicy 等方式实现。具体方案取决于部署架构,这里只给一个排查命令示例,用于确认服务所在环境的路由情况:

ip route
ip addr
curl -I --max-time 3  payload 是否能打通”,而是验证安全策略是否稳定生效。

测试项预期结果
302 到内网地址拒绝,重定向后重新校验
超大响应体中断下载并记录日志
非图片 Content-Type拒绝

单元测试中可以用本地 mock server 模拟不同响应,不需要访问真实敏感地址。例如使用 WireMock、MockWebServer 或框架自带测试服务器,构造 200、302、超大响应、错误 Content-Type 等场景。

日志与告警

SSRF 类问题修复后,建议补充必要日志,但不要记录敏感响应内容。比较有价值的字段包括:

  • 用户 ID 或业务主体 ID。
  • 原始 URL。
  • 最终请求 URL。
  • 解析后的 IP。
  • 拒绝原因。
  • 响应状态码与响应大小。
  • 请求耗时。

如果频繁出现访问回环地址、私有地址、非常规协议的请求,可以作为风险行为告警。但告警要避免过度噪声,最好结合用户、接口、频率做聚合。

代码审计中的几个经验点

  • 不要只搜 SSRF 关键字,应该搜 URL、URI、openConnection、RestTemplate、HttpClient、WebClient、OkHttp、Jsoup.connect 等调用点。
  • 后台功能也要审计,很多 SSRF 出现在运营配置、接口调试、数据同步工具里。
  • 图片处理链路要关注第三方库是否会继续加载外部资源。
  • PDF、HTML 转换服务风险较高,渲染引擎可能自动加载 CSS、图片、字体等资源。
  • 安全校验不要写在 Controller 层后就结束,底层请求组件仍应具备自保护能力。

总结

SSRF 的治理重点是明确服务端请求边界。只靠黑名单或字符串过滤很容易漏,比较可靠的做法是:能白名单就白名单;必须支持任意公网 URL 时,做协议、DNS、IP、重定向、响应大小、内容类型等完整校验;同时在网络层限制出站访问范围。

从长期维护角度看,建议把安全请求逻辑封装成统一组件,并配套单元测试和审计日志。这样后续新增“远程 URL 导入”“链接预览”“Webhook 测试”这类功能时,不需要每个业务团队重复踩坑。

服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…

SiteMap HACKHAT © 2026 All rights reserved.