跳转到帖子

背景

SSRF(Server-Side Request Forgery)在实际业务里经常不是一个单点漏洞,而是一类“服务端可控请求”能力被滥用后的结果。常见入口包括图片抓取、URL 预览、Webhook 回调、文件导入、第三方接口代理、PDF 生成等。很多团队在修复时只做了简单黑名单,比如拦截 127.0.0.1 或 localhost,但在真实环境里,这类修复往往不够稳定。

这篇文章整理一次内部排查 SSRF 风险时比较通用的方法,重点放在代码审计、验证思路和防护落地。内容仅用于授权测试和防御建设,不涉及未授权攻击操作。

问题场景

某业务提供“根据 URL 抓取远程图片并上传到对象存储”的功能。用户提交一个图片地址,后端下载文件,校验类型后保存。入口看起来比较正常,但它具备三个典型 SSRF 条件:

  • 用户可以控制请求目标 URL。
  • 服务端会主动发起网络请求。
  • 请求发生在内网环境,服务端网络权限高于普通用户。

简化后的代码逻辑大致如下:

String url = request.getParameter("url");
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
InputStream in = conn.getInputStream();
// 后续做图片类型判断与上传

这类实现的问题在于,代码只关心“能否下载到内容”,但没有限制协议、地址范围、跳转行为和解析结果。攻击面不是来自 HTTP 客户端本身,而是来自“把网络访问能力暴露给了用户输入”。

技术分析

排查 SSRF 时,我一般会从四个层面看:入口、解析、请求、出网。

1. 入口:哪些参数会触发服务端请求

代码审计时,可以优先搜索以下关键字:

URL
URI
openConnection
HttpClient
OkHttpClient
RestTemplate
WebClient
curl_exec
file_get_contents
requests.get
axios
fetch
S3 import
webhook
callback
avatar
preview
thumbnail

需要特别注意“间接触发”的场景。例如用户上传 HTML 后,后端生成 PDF;用户填写头像地址,异步任务去下载;配置 Webhook 后,测试按钮会立即发请求。这些入口有时不在主业务 Controller,而是在队列消费者、定时任务或管理后台里。

2. 解析:URL 校验与实际请求不一致

不少 SSRF 修复失败,是因为校验用一套解析逻辑,请求又用另一套解析逻辑。比如先用正则判断 URL 字符串,再交给 HTTP 客户端请求。URL 的编码、大小写、用户名密码段、重定向、DNS 解析都会带来差异。

建议统一使用标准库解析,并对解析后的结构做判断,不要直接对原始字符串做粗略匹配。校验内容至少包括:

  • scheme 只允许 http、https。
  • host 必须存在,禁止空 host。
  • 禁止 userinfo,例如 80、443。
  • 禁止访问内网、回环、链路本地、保留地址。
  • 禁止或严格处理重定向。

示例伪代码如下:

URI uri = URI.create(inputUrl);
String scheme = uri.getScheme();
String host = uri.getHost();

if (!List.of("http", "https").contains(scheme)) {
    throw new IllegalArgumentException("unsupported scheme");
}
if (host == null || uri.getUserInfo() != null) {
    throw new IllegalArgumentException("invalid url");
}

int port = uri.getPort();
if (port != -1 && port != 80 && port != 443) {
    throw new IllegalArgumentException("unsupported port");
}

3. 请求:DNS 解析、重定向与最终 IP

仅检查 host 字符串是不够的。业务允许访问 example.com,但该域名可能解析到内网地址;或者第一次解析是公网地址,请求过程中发生重定向,最终跳到内部地址。因此防护应基于“最终连接 IP”做判断。

比较稳妥的策略是:

  • 对 host 做 DNS 解析,获取全部 A/AAAA 记录。
  • 逐个判断 IP 是否属于禁止网段。
  • 请求前关闭自动重定向,手动检查 Location。
  • 如果允许跳转,每一跳都重复完整校验。
  • 限制跳转次数,例如最多 3 次。

常见应禁止的地址范围包括:

类型示例
IPv4 回环127.0.0.0/8
IPv4 私有地址10.0.0.0/8、172.16.0.0/12、192.168.0.0/16
链路本地169.254.0.0/16
本机未指定地址0.0.0.0/8
IPv6 回环::1/128
IPv6 链路本地fe80::/10
IPv6 本地地址fc00::/7

Java 中可以结合 InetAddress 做基础判断,但不要只依赖 isSiteLocalAddress。不同语言和库的判断覆盖面不完全一致,建议维护统一的 IP 网段判断工具。

InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
    if (isBlockedAddress(address)) {
        throw new SecurityException("blocked target address");
    }
}

4. 出网:不要把防护完全压在业务代码里

业务层校验很重要,但不能作为唯一防线。更可靠的做法是把出网能力收敛到网关或代理层,例如:

  • 应用服务器默认不允许直接访问内网敏感网段。
  • 统一通过 egress proxy 出网。
  • 代理层做域名白名单或分类控制。
  • 限制访问云元数据地址、内部管理地址、服务发现地址。
  • 为抓取类任务单独部署低权限网络环境。

如果是 Kubernetes 环境,可以结合 NetworkPolicy、Service Mesh 或云厂商安全组限制 Pod 出网。原则是:即使业务代码漏了一处,网络层也不应该让它直接访问敏感目标。

关键步骤

实际排查时,可以按下面流程推进。

步骤一:梳理所有服务端请求入口

先不要急着写 payload,先把入口列清楚。建议按业务功能建立清单:

  • 用户输入字段:URL、callback、webhook、avatar、image、file。
  • 后台配置项:第三方接口地址、推送地址、同步地址。
  • 异步任务:导入、转码、截图、PDF、爬取。
  • 代理接口:通用转发、开放 API 调试工具。

每个入口记录调用链、HTTP 客户端、是否跟随跳转、是否走代理、是否有超时和大小限制。

步骤二:做最小化验证

在授权环境中,可以搭建一个受控的测试服务,用来观察服务端是否发起请求、请求头是什么、是否跟随跳转。不要用第三方不可控地址,也不要探测非授权内网资源。

# 本地测试服务,仅用于授权环境观察请求
python3 -m http.server 8080

如果需要观察请求头,可以用简单脚本记录:

from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        print(self.path)
        print(self.headers)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"ok")

HTTPServer(("0.0.0.0", 8080), H).serve_forever()

步骤三:检查重定向处理

很多客户端默认跟随 301、302、307、308。修复时建议关闭自动跳转,手动处理 Location,并对每一跳重新执行 URL 与 IP 校验。

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

如果业务没有强需求,直接禁止跳转是最简单、最稳定的方案。确实需要跳转时,要限制跳转次数,避免循环跳转和长链路消耗资源。

步骤四:限制响应体和内容类型

抓取图片、文档这类功能容易被大文件拖垮。除了 SSRF,还要关注资源消耗风险。建议设置:

  • 连接超时和读取超时。
  • 最大响应体大小。
  • 允许的 Content-Type。
  • 文件魔数校验。
  • 下载完成后再进入后续处理。
int maxBytes = 5 * 1024 * 1024;
byte[] buf = new byte[8192];
int total = 0;
int n;
while ((n = in.read(buf)) != -1) {
    total += n;
    if (total > maxBytes) {
        throw new IOException("file too large");
    }
    out.write(buf, 0, n);
}

步骤五:补充日志与告警

SSRF 风险排查不是一次性工作。建议记录以下字段,方便后续追踪:

  • 业务入口和用户标识。
  • 原始 URL。
  • 解析后的 scheme、host、port。
  • DNS 解析结果。
  • 最终请求 IP。
  • 重定向链路。
  • 拦截原因。

日志里不要记录敏感凭据。如果 URL 可能包含 token,需要做脱敏。

注意点

  • 不要只用字符串黑名单。localhost、大小写、编码、IPv6、重定向都可能导致判断失效。
  • 不要只校验第一次 URL。重定向后的地址同样需要完整校验。
  • 不要允许任意协议。file、gopher、ftp 等协议通常不应出现在普通业务抓取场景中。
  • 不要忽视 IPv6。很多拦截逻辑只覆盖 IPv4。
  • 不要让抓取服务和核心业务服务处在同一高权限网络区域。
  • 不要无限制读取响应体。SSRF 经常和拒绝服务风险一起出现。
个人经验里,SSRF 的修复质量主要取决于两点:一是是否基于解析后的最终连接目标做判断,二是网络层有没有兜底。只靠正则补丁,后续基本还会返工。

总结

SSRF 防护不应该只理解为“拦几个内网 IP”。更完整的处理方式是:入口收敛、URL 标准化解析、DNS 与最终 IP 校验、重定向逐跳检查、响应资源限制、出网网络隔离、日志持续监控。

在代码审计中,凡是“用户输入影响服务端请求目标”的逻辑,都应该进入重点清单。对业务来说,最实用的方案不是把校验写得很复杂,而是尽量减少可控请求能力,并把必须存在的能力放到低权限、可观测、可限制的环境里。

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

0篇意见

推荐意见

没有意见。

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