这篇文章整理的是一次内部资产管理系统代码审计中遇到的 SSRF 风险排查过程。场景比较常见:系统提供 URL 探测、截图、Webhook 回调、文件远程导入等功能,本意是提升运维效率,但如果边界处理不严,很容易变成访问内部资源的跳板。
本文只讨论代码审计、风险定位和修复思路,示例均基于本地环境和伪代码,不涉及对真实目标的攻击操作。
背景
被审计系统是一个内网资产平台,主要功能包括:
- 录入资产 URL 后自动探测标题、状态码、证书信息;
- 支持从远程 URL 导入资产清单;
- 支持配置 Webhook,将扫描结果推送到第三方服务;
- 后台有部分运维接口会请求指定地址做连通性检查。
这类系统有一个共同点:服务端会主动发起 HTTP 请求。只要请求地址可被用户影响,就需要重点关注 SSRF。
审计 SSRF 时,不要只盯着“访问 URL”这几个字。截图、预览、导入、回调、探测、健康检查、代理转发、PDF 生成、图片抓取,本质上都可能是服务端请求外部资源。
问题场景
代码中有一个远程导入功能,用户提交一个 URL,后端下载 CSV 内容并解析资产信息。简化后的逻辑如下:
public List<Asset> importFromUrl(String url) {
String body = httpClient.get(url);
return csvParser.parse(body);
}从功能角度看没有明显问题,但安全边界缺失:
- 没有限制协议,可能接受非预期 scheme;
- 没有校验目标 IP 是否属于内网、回环、链路本地等地址段;
- 没有处理 DNS 解析结果变化;
- 没有限制重定向后的最终地址;
- 没有设置合理的超时、响应大小和内容类型限制;
- 错误信息可能把内部连接结果直接返回给前端。
技术分析
SSRF 的核心不是“能不能访问某个危险地址”,而是“用户输入是否影响了服务端网络请求的目标”。因此审计时可以从数据流入手:
- Source:URL、Webhook 地址、图片地址、远程配置地址、代理目标地址;
- Transform:拼接、解码、重定向、DNS 解析、短链接展开;
- Sink:HttpClient、OkHttp、RestTemplate、URLConnection、curl、wget、Headless Browser;
- Boundary:协议、域名、IP、端口、重定向、超时、响应大小。
在这次审计中,重点排查了下面几类调用:
# Java 项目中常见的网络请求入口,可用于第一轮粗筛
grep -R "new URL\|openConnection\|RestTemplate\|OkHttpClient\|HttpClient\|WebClient" -n src/main/java
# 如果项目里有命令执行封装,也要关注是否调用了 curl/wget
grep -R "curl\|wget" -n src/main/java src/main/resources粗筛之后不要急着下结论,要继续看参数来源。如果参数来自固定配置,风险通常较低;如果来自用户可控字段,就要继续看校验逻辑是否完整。
容易遗漏的细节
| 风险点 | 常见误区 | 建议处理 |
|---|---|---|
| 协议限制 | 只判断字符串以 http 开头 | 使用 URI 解析后严格允许 http/https |
| 域名校验 | 只做后缀匹配或 contains 判断 | 解析后取 host,结合 allowlist |
| IP 校验 | 只校验原始输入 | DNS 解析后校验所有返回 IP |
| 重定向 | 初始地址合法就放行 | 每一次重定向都重新校验 |
| DNS Rebinding | 解析一次后直接请求 | 请求前解析并绑定校验结果,必要时禁用不可信域名 |
| 响应处理 | 无限制读取响应体 | 限制大小、类型、超时 |
关键步骤
实际审计时,我一般按下面顺序推进,效率会比较稳定。
1. 找到所有服务端请求点
先用关键字扫一遍,再结合路由和业务功能确认是否可被外部输入影响。以 Java 为例,除了直接 HTTP 客户端,还要看图片处理、文档转换、浏览器截图等依赖。
# 控制器层入口
find src/main/java -type f | xargs grep -n "@RequestMapping\|@PostMapping\|@GetMapping"
# 常见 URL 参数名
find src/main/java -type f | xargs grep -n "url\|callback\|webhook\|redirect\|remote\|import"2. 看校验是否发生在正确位置
很多代码看似做了校验,但位置不对。例如先请求、后判断内容类型;先跟随重定向、后校验最终 URL;先把 URL 交给第三方库预处理、后做字符串判断。这些都不可靠。
推荐的顺序是:解析 URI、校验 scheme、校验 host、解析 IP、校验 IP 段、创建连接、禁用自动重定向或手动校验重定向、限制响应。
3. 不要依赖简单字符串判断
下面这种写法在代码里很常见,但不建议作为安全边界:
if (!url.startsWith('http')) {
throw new IllegalArgumentException('invalid url');
}
if (url.contains('127.0.0.1')) {
throw new IllegalArgumentException('blocked');
}原因是 URL 表达形式很多,字符串匹配很容易漏掉编码、大小写、用户名密码段、IPv6、十进制或八进制 IP、重定向等情况。更稳妥的做法是基于标准解析器和网络库返回的结构化结果进行判断。
4. 建立统一的安全请求封装
不要让各业务模块自己写一套 URL 校验。建议提供一个统一的 SafeHttpClient,把协议、解析、IP 段、重定向、超时、大小限制都放进去。
public Response safeGet(String rawUrl) {
URI uri = URI.create(rawUrl.trim());
if (!List.of('http', 'https').contains(uri.getScheme())) {
throw new SecurityException('scheme not allowed');
}
String host = uri.getHost();
if (host == null || host.isBlank()) {
throw new SecurityException('host required');
}
List<InetAddress> addresses = resolveAll(host);
for (InetAddress address : addresses) {
if (isPrivateOrReserved(address)) {
throw new SecurityException('private address not allowed');
}
}
return httpClient
.withConnectTimeout(3000)
.withReadTimeout(5000)
.withMaxBodySize(1024 * 1024)
.disableAutoRedirect()
.get(uri);
}上面只是示意,真实项目里还需要补充 IPv6、代理配置、证书校验、异常处理、日志脱敏等细节。
5. 重定向必须逐跳校验
如果业务确实需要支持 301/302,不能简单开启自动重定向。应当手动处理 Location,并对每一跳重新走完整校验流程,同时限制跳转次数。
int redirects = 0;
URI current = inputUri;
while (redirects < 3) {
validateUri(current);
Response resp = requestWithoutAutoRedirect(current);
if (!resp.isRedirect()) {
return resp;
}
current = current.resolve(resp.header('Location'));
redirects++;
}
throw new SecurityException('too many redirects');6. 对业务做正向约束
如果业务场景明确,例如 Webhook 只允许推送到企业自有域名,最有效的方案是 allowlist,而不是试图维护一个越来越长的 blocklist。
- 远程导入:优先使用文件上传,不一定要支持任意 URL;
- Webhook:允许用户配置域名,但后台审批或绑定固定域名;
- 资产探测:只允许探测已登记资产,不允许任意地址;
- 截图服务:放到隔离网络,不能访问管理网段和云元数据地址;
- 内部健康检查:目标从配置中心读取,不从外部请求参数读取。
注意点
- SSRF 修复不要只改一个接口。建议梳理整个项目里的服务端请求能力,统一封装。
- 如果服务运行在云环境,必须关注元数据服务访问风险,并在网络层做限制。
- 容器环境里要关注访问宿主机、Kubernetes API、Service 网段等路径。
- 日志里记录原始 URL 时要脱敏,避免泄露 token、签名参数。
- 错误信息不要把连接失败原因、内部 IP、响应头完整返回给前端。
- 测试环境可以用本地 mock 服务验证校验逻辑,不要对未知第三方地址做探测。
一个实用的自检清单
| 检查项 | 是否建议强制 |
|---|---|
| 仅允许 http/https | 是 |
| 禁止访问私有地址、回环地址、链路本地地址、保留地址 | 是 |
| DNS 解析结果全部校验 | 是 |
| 重定向逐跳校验 | 是 |
| 限制端口范围 | 建议 |
| 设置连接和读取超时 | 是 |
| 限制响应体大小 | 是 |
| 统一请求封装 | 是 |
| 网络层隔离高敏网段 | 是 |
| 安全日志和告警 | 建议 |
总结
SSRF 在代码层看起来往往只是一个“请求 URL”的小功能,但在内网系统里影响会被放大。审计时要抓住两点:一是用户输入是否能影响服务端请求目标,二是请求前后是否存在完整的边界控制。
从长期维护角度看,最值得投入的是统一安全请求封装和网络层隔离。前者减少业务代码重复犯错,后者能在代码漏防时兜底。单靠某个接口里的字符串判断,通常扛不住复杂场景。
推荐意见