跳转到帖子

URL 预览、远程图片抓取、Webhook 测试和文档转码等功能,都需要由服务端访问用户提交的地址。这类功能实现起来并不复杂,但如果缺少严格的目标校验,就可能演变为服务端请求伪造(SSRF)风险。

SSRF 的危险不只在于访问内网地址。实际事件中,更常见的问题是:校验逻辑只检查字符串、忽略重定向、DNS 解析结果可变、IPv6 处理不完整,以及出站网络权限过大。本文以一次典型的 URL 预览服务为例,整理从代码审计、受控验证到修复落地的完整思路。

一、问题背景与典型场景

某应用提供 URL 预览接口,用户提交一个页面地址,后端抓取标题、描述和首张图片,再返回给前端展示。简化后的代码如下:

public PreviewResult preview(String input) throws IOException {
    URL url = new URL(input);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    conn.setInstanceFollowRedirects(true);

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

表面上看,代码设置了连接和读取超时,也限制了响应处理时间,但它没有解决几个核心问题:

  • 没有限制协议,file、ftp 等非 HTTP 协议可能被意外处理。
  • 没有限制目标地址,服务端可能访问本机、内网或云环境管理地址。
  • 开启了自动重定向,初始地址即使通过校验,也可能跳转到不允许的地址。
  • 没有限制响应大小,可能造成内存消耗或带宽占用。
  • DNS 解析和连接过程缺少一致性校验,存在解析结果变化导致的绕过空间。

二、代码审计时的关注点

审计 SSRF 时,不要只搜索名称中包含 URL、request 或 fetch 的函数。更有效的做法是从“用户可控数据如何进入网络客户端”这条数据流入手。

1. 识别输入来源

重点检查以下位置:

  • HTTP 参数、JSON 字段、表单字段和文件元数据。
  • 数据库中可编辑的回调地址、头像地址、Webhook 地址。
  • 消息队列、任务系统和管理后台传入的远程资源地址。
  • 从 HTML、Markdown、RSS 或文档内容中提取出的外部链接。

2. 识别实际请求组件

常见的请求组件包括 Java 的 URLConnection、Apache HttpClient、OkHttp,Python 的 requests、urllib,Node.js 的 axios、fetch,以及各类浏览器自动化框架。需要继续跟踪组件的默认行为,例如是否自动跟随重定向、是否支持代理、是否解析特殊协议,以及是否会在连接前重新解析域名。

3. 检查校验位置

校验必须发生在真正建立连接之前,并且不能只依赖前端或单一业务层。以下做法通常不够可靠:

  • 仅判断字符串是否以 http:// 或 https:// 开头。
  • 仅禁止 127.0.0.1、localhost 等少量字面值。
  • 先解析域名做一次检查,随后让 HTTP 客户端自行再次解析并连接。
  • 只校验初始 URL,不校验每一次重定向。
  • 使用黑名单阻止少数内网网段,却没有明确允许的协议、端口和目标范围。

三、受控验证思路

安全验证应在自有测试环境或明确授权的范围内完成。建议先搭建一个本地测试服务,分别模拟正常站点、重定向站点和禁止访问的地址,不要直接对生产环境、云平台管理地址或不属于自己的网络进行测试。

验证目标不是读取敏感数据,而是确认应用是否按照预期执行了出站请求。可以在测试环境中记录以下信息:

  • 请求是否真正发出。
  • 请求使用的协议、主机和端口。
  • 是否发生了重定向。
  • 最终响应的大小、状态码和处理耗时。
  • 应用是否能够区分“地址格式错误”“目标被禁止”和“远端不可用”。

测试用例可以覆盖以下类别:

类别验证内容预期结果
正常公网 HTTPS 地址基础功能是否可用在大小、超时和内容类型限制内正常返回
非 HTTP 协议协议限制是否生效在连接前拒绝
环回、私有和链路本地地址内网目标限制是否生效在解析或连接前拒绝
指向受限地址的重定向每跳重定向是否重新校验拒绝跳转或终止请求
超大响应和慢响应资源消耗控制是否有效达到阈值后及时终止

四、稳健的校验策略

1. 先解析 URL,再检查结构

不要使用简单字符串匹配代替 URL 解析。至少应检查协议、主机名、端口、用户名密码字段和路径。通常应拒绝 URL 中包含用户信息的形式,避免日志、代理或解析组件对地址产生不同解释。

URI uri = URI.create(input.trim());
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();

if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
    throw new SecurityException("unsupported scheme");
}
if (host == null || uri.getUserInfo() != null) {
    throw new SecurityException("invalid target");
}
if (port != -1 && port != 80 && port != 443) {
    throw new SecurityException("unsupported port");
}

实际项目中还要考虑大小写、尾随点、国际化域名、IPv4 映射 IPv6、异常编码等情况。URL 解析器之间的差异也需要通过测试用例固定下来。

2. 对域名解析结果进行地址分类

对于域名目标,应解析其全部 A 和 AAAA 记录,并检查每一个结果。不能只检查第一个 IPv4 地址,也不能忽略 IPv6。

需要根据业务场景禁止环回地址、未指定地址、私有地址、链路本地地址、多播地址以及其他不应访问的保留网段。地址分类最好使用经过审查的网络库,而不是手工拼接正则表达式。

如果业务只需要访问固定的合作方,优先采用域名白名单或固定 IP 白名单。允许列表的范围越小,后续风险越容易控制。

3. 处理 DNS 解析与连接的一致性

仅在业务层解析一次域名,再把原始域名交给底层 HTTP 客户端,可能导致校验结果与实际连接目标不一致。更稳妥的方式包括:

  • 在受控网络客户端中完成解析、地址分类和连接,避免中间环节重新解析到未知地址。
  • 对每个解析结果进行校验,并限制连接只能使用通过校验的地址。
  • 在网络层增加出站访问控制,作为应用校验之外的第二道边界。
  • 对 DNS 缓存时间、连接复用和代理行为进行明确配置。

如果业务无法保证解析与连接的一致性,至少应把应用层校验与网络层隔离同时落实,不能把安全性完全寄托在一次 DNS 查询上。

4. 严格处理重定向

不要无条件开启自动重定向。更合理的方式是由应用接管重定向流程,每跳都重新解析和校验目标,并设置最大跳数。

URI current = initialUri;
for (int i = 0; i <= 3; i++) {
    validateTarget(current);
    HttpResponse response = requestOnce(current);

    if (!isRedirect(response.statusCode())) {
        return readLimitedBody(response.body(), 1024 * 1024);
    }

    String location = response.header("Location");
    if (location == null) {
        throw new IOException("redirect without location");
    }
    current = resolveAndNormalize(current, location);
}
throw new SecurityException("too many redirects");

重定向地址可能是相对路径,也可能改变协议、端口或主机。每一次跳转都应执行完整校验,而不是只检查 Location 字段是否为空。

五、资源消耗与内容处理

SSRF 防护不能只关注网络位置。远程内容本身也可能带来拒绝服务风险,因此需要同时设置连接超时、读取超时、最大响应大小、最大响应头数量、最大重定向次数和并发限制。

  • 使用流式读取,达到字节上限后立即终止连接。
  • 限制响应内容类型,只处理业务真正需要的类型。
  • 不要根据 Content-Length 单独判断大小,分块传输可能没有该字段。
  • 对 HTML、XML、SVG 等可能触发二次解析的格式关闭不必要的外部实体和外部资源加载。
  • 对预览任务设置队列并发上限,避免大量慢请求拖垮工作线程。
  • 记录请求耗时和终止原因,便于发现异常探测或配置问题。

六、网络层隔离比代码修补更可靠

应用层校验容易遗漏边界情况,因此 URL 抓取服务最好运行在独立的低权限环境中。该环境不应拥有访问数据库、缓存、消息队列和云管理接口的网络权限。

常见的隔离措施包括:

  • 通过出口代理统一控制允许访问的域名、端口和协议。
  • 使用防火墙或安全组默认拒绝出站流量,只放行明确需要的目标。
  • 将抓取任务放到独立容器或虚拟机中,使用非特权用户运行。
  • 限制服务账号权限,不向该环境注入云平台访问令牌或内部配置。
  • 对异常 DNS、连接失败、禁止目标命中和重定向链进行集中审计。

应用层负责判断“这个请求是否符合业务规则”,网络层负责保证“即使应用判断错误,也不能触达高价值内部资源”。两者缺一不可。

七、常见修复误区

  • 用黑名单替代白名单:攻击面会随解析器差异、地址格式和新增网段不断扩大。
  • 只禁用 localhost:私有地址、IPv6 环回、链路本地地址和云环境特殊网段同样需要考虑。
  • 只检查 URL 文本:文本看起来正常,并不代表最终连接目标安全。
  • 关闭重定向但不处理业务跳转:如果业务确实需要重定向,就必须实现逐跳校验。
  • 只在网关层修复:内部任务、异步消费者或其他入口仍可能绕过网关直接调用抓取逻辑。
  • 修复后没有回归测试:后续更换 HTTP 客户端或调整代理配置时,原有防护可能失效。

八、回归测试与监控建议

可以把 SSRF 防护整理成自动化测试集合,在每次依赖升级或网络配置变更时执行。测试至少应覆盖正常地址、非法协议、不同地址族、私有地址、重定向、超时、超大响应和 DNS 异常等场景。

日志中不建议记录完整的敏感查询参数或响应正文,但可以记录经过脱敏和规范化后的目标类型、解析结果分类、响应状态、耗时、响应大小和拒绝原因。监控侧重点包括:

  • 短时间内大量不同域名或不同端口的访问请求。
  • 禁止目标命中次数突然升高。
  • 重定向次数、连接超时和读取超时异常增加。
  • 抓取服务向非业务所需网段发起连接。
  • 单个用户、任务或来源持续消耗大量抓取资源。

总结

SSRF 的核心问题不是某一个特殊字符串,而是“用户输入影响了服务端的网络边界”。可靠的治理方案应当从数据流审计开始,结合严格的 URL 解析、协议和端口限制、完整 DNS 地址校验、逐跳重定向检查、响应资源限制,以及网络层出站隔离。

在实际项目中,最值得优先落地的顺序通常是:先明确业务允许访问的目标范围,再关闭不必要的协议和重定向,随后补齐地址分类与资源限制,最后通过出口控制和自动化回归测试形成长期防线。这样即使某一层校验出现遗漏,也不会直接演变为对内部系统的广泛访问。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

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