背景
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 校验、重定向逐跳检查、响应资源限制、出网网络隔离、日志持续监控。
在代码审计中,凡是“用户输入影响服务端请求目标”的逻辑,都应该进入重点清单。对业务来说,最实用的方案不是把校验写得很复杂,而是尽量减少可控请求能力,并把必须存在的能力放到低权限、可观测、可限制的环境里。
推荐意见