SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器发一个 HTTP 请求”这么简单。真正难处理的地方在于:业务往往确实需要访问外部资源,而攻击者能够影响请求目标、解析过程或重定向链路。审计时如果只搜索 HttpClient、requests.get 之类的调用,很容易漏掉经过多层封装的风险;修复时如果只增加一个字符串黑名单,也通常不够可靠。
本文以“服务端根据用户提交的 URL 抓取图片并生成缩略图”这一常见场景为例,整理一套偏工程化的分析方法。示例只用于本地测试和防御验证,不针对任何真实系统或第三方服务。
一、典型业务场景与风险边界
常见代码结构如下:
String imageUrl = request.getParameter("url");
byte[] data = httpClient.get(imageUrl);
return resize(data);从业务角度看,这段代码可能用于远程头像、图片转存、网页截图或文档解析。问题在于,url 由客户端控制,服务端网络权限通常又高于普通用户,因此请求可能被引导到不应暴露的内部资源。
风险通常包括以下几类:
- 服务端可访问的内网管理界面、开发接口或调试端口被间接探测。
- 云环境中的实例身份服务、内部配置服务等敏感位置被错误访问。
- 服务端自身的 Cookie、认证头或网络位置被用于访问内部接口。
- 大量慢请求、超大响应或连接复用不当,造成资源耗尽。
- 图片、文档等后续解析环节继续引入文件格式、压缩包或解析器风险。
需要注意,SSRF 不等于一定能够读取敏感数据。实际影响取决于网络分区、出口策略、目标服务认证方式、客户端是否自动跟随重定向,以及响应内容是否会返回给用户。
二、审计时先画出完整的数据流
比起直接搜索危险函数,更有效的做法是从输入源追踪到网络出口,至少梳理以下节点:
- 输入源:HTTP 参数、JSON 字段、数据库记录、消息队列内容、Webhook 配置和后台任务参数。
- 规范化过程:是否进行 URL 解码、大小写转换、去除空白、拼接默认协议。
- 解析过程:使用何种 URL 解析器,是否区分主机名、端口、用户信息和路径。
- DNS 过程:域名解析发生在哪里,是否会重复解析,解析结果是否被重新校验。
- 请求过程:是否允许任意协议、任意端口、跨协议跳转或自动重定向。
- 响应处理:是否限制状态码、内容类型、响应大小和读取时间。
- 输出位置:响应是否直接返回给用户,还是写入对象存储、消息队列或日志。
实际项目中,危险调用经常被封装成“下载器”“资源加载器”“预览服务”或“连接器”。建议同时检索以下特征:
URL、URI、URI.create、openConnection、HttpClient、WebClient
requests.get、urllib、aiohttp、fetch、axios、RestTemplate
followRedirects、redirect、proxy、download、fetch、preview、import审计结果最好记录成一条可复核的数据流,例如:
请求参数 url → URL 解码 → ImageDownloader.fetch → Apache HttpClient → 自动跟随重定向 → 返回字节流 → 图片解析器。
三、最容易被忽略的解析差异
SSRF 防护失败,很多时候不是没有校验,而是校验和实际发请求使用了不同的解析规则。例如,业务代码使用字符串判断主机名,底层 HTTP 客户端却按照标准 URL 语法解析。
重点关注以下情况:
- 用户信息字段:
[email protected]中真正的主机部分不是前面的字符串。 - 端口和协议:只校验域名而不限制端口,可能导致访问非预期服务。
- IPv4 的多种表示:十进制整数、八进制、十六进制或混合表示可能被不同库解析成同一个地址。
- IPv6 表示:需要正确处理方括号、压缩写法和 IPv4 映射地址。
- 重定向:初始 URL 合法,不代表 301、302 或其他跳转后的目标仍然合法。
- DNS 重绑定:域名校验时得到公网地址,实际连接时解析结果却发生变化。
- 大小写、尾点和国际化域名:域名规范化不一致可能导致 allowlist 判断偏差。
因此,不能仅依赖如下形式的判断:
if (url.startsWith(" {
fetch(url);
}更稳妥的原则是:先使用统一解析器得到结构化结果,再对协议、主机、端口和解析地址进行策略判断,最后让实际连接过程使用同一套经过验证的目标信息。
四、推荐的防护策略:白名单优先,出口策略兜底
如果业务只需要访问有限的几个合作方或对象存储域名,应优先采用精确白名单,而不是维护不断扩大的黑名单。策略至少应包含协议、主机和端口三个维度。
| 检查项 | 建议 | 原因 |
|---|---|---|
| 协议 | 通常只允许 HTTPS,必要时单独评估 HTTP | 避免文件协议、脚本协议或其他处理器被调用 |
| 主机 | 使用规范化后的精确域名白名单 | 避免后缀匹配造成越权 |
| 端口 | 只允许明确需要的端口 | 降低访问内部非 Web 服务的风险 |
| 解析地址 | 拒绝回环、链路本地、私有和保留地址 | 防止域名解析到内部网络 |
| 重定向 | 禁用或逐跳重新校验,限制跳转次数 | 避免通过跳转绕过首个 URL 的检查 |
| 响应 | 限制大小、类型、读取时间和总耗时 | 降低资源耗尽和解析器风险 |
五、一个更安全的实现思路
下面示例展示的是防御性伪代码,重点在于校验顺序和策略边界。生产环境中还需要结合具体语言的 URL、DNS 和 HTTP 客户端实现进行测试。
URI target = parseStrictly(input);
if (!equalsIgnoreCase(target.scheme(), "https")) {
throw new SecurityException("scheme not allowed");
}
String host = normalizeDnsName(target.host());
int port = target.port() == -1 ? 443 : target.port();
if (!ALLOWED_HOSTS.contains(host)) {
throw new SecurityException("host not allowed");
}
if (!ALLOWED_PORTS.contains(port)) {
throw new SecurityException("port not allowed");
}
List<IPAddress> addresses = resolveAll(host);
for (IPAddress address : addresses) {
if (isLoopback(address)
|| isLinkLocal(address)
|| isPrivate(address)
|| isReserved(address)) {
throw new SecurityException("destination address not allowed");
}
}
HttpClient client = buildClient(
connectTimeout = 3s,
readTimeout = 5s,
maxRedirects = 0,
maxResponseBytes = 5 * 1024 * 1024
);
return client.get(target);这里有几个关键点:
- 不要为了“兼容用户输入”而自动补全危险协议。无法明确解析的输入应直接拒绝。
- 域名白名单应使用规范化后的完整主机名,避免简单的后缀判断。
- 如果业务必须支持重定向,应对每一次跳转重新执行协议、主机、端口和地址检查。
- DNS 校验和实际连接之间存在竞态时,优先通过受控出口代理或网络层策略降低风险。
- 客户端应关闭不需要的代理继承,避免环境变量或系统代理改变请求路径。
六、验证修复时如何搭建安全的本地测试
验证 SSRF 修复不需要连接真实内网。可以在本机或隔离网络中启动两个测试服务:
- 一个模拟允许访问的外部资源服务,返回固定的图片或文本。
- 一个模拟禁止访问的内部服务,只返回测试标记,不包含任何真实凭据。
测试用例应覆盖正常输入和边界输入,而不是只测试一个被拦截的地址。推荐至少包含:
- 允许列表中的标准 HTTPS URL。
- 缺少协议、协议大小写变化和包含空白字符的输入。
- 包含用户信息、显式端口和路径变化的 URL。
- 解析到回环、链路本地、私有或保留地址的域名。
- 从允许域名跳转到禁止目标的重定向链。
- 超大响应、慢响应和错误 Content-Type。
- 多次请求同一域名时 DNS 结果变化的情况。
如果使用命令行验证,建议只针对本地测试服务,例如:
curl --max-time 5 \
--connect-timeout 2 \
-H 'Accept: image/*' \
' 403”。还应记录最终请求目标、解析地址、是否发生重定向、响应大小、耗时和拒绝原因。这样才能判断防护是在正确的环节生效,而不是被某个前置规则偶然拦截。七、日志、监控与应急处置
SSRF 防护需要可观测性支持。建议记录以下字段,但避免把完整的认证信息或敏感查询参数写入日志:
- 请求来源、业务接口和关联请求 ID。
- 规范化后的协议、主机和端口。
- 解析得到的地址类别,例如公网、私有、回环或保留地址。
- 拒绝原因、重定向次数、响应状态码和响应大小。
- 连接耗时、读取耗时及超时类型。
如果线上出现异常,应先确认是否存在大量不同目标、集中访问内部地址、异常重定向或响应体积突增,再结合出口防火墙、代理和应用日志进行关联。临时措施可以包括收紧允许域名、暂停非核心远程抓取功能、限制出口端口和增加请求速率限制。修复后要补充回归测试,避免业务方为了恢复功能而重新打开宽泛访问。
八、常见但不充分的修复方式
- 只拦截几个字符串:无法覆盖不同的地址表示、解析差异和重定向。
- 只判断响应状态码:请求已经发出后,敏感目标可能已经被访问。
- 只在前端校验:前端限制不构成服务端安全边界。
- 只禁止一个协议:即使只允许 HTTP/HTTPS,仍需检查目标地址、端口和跳转。
- 只校验一次 DNS:解析和连接之间可能存在时间窗口。
- 将下载服务放在高权限网络:应用层规则失效时,网络隔离仍应提供最后一道防线。
九、从代码审计报告到可落地整改
一份有价值的审计结论应尽量包含完整证据链:
- 输入点和调用链位置。
- 实际使用的 URL 解析器和 HTTP 客户端配置。
- 是否支持重定向、代理、跨协议和自定义端口。
- 响应是否返回给用户或进入后续解析流程。
- 影响范围,包括可访问网络区域和可能暴露的数据类型。
- 最小化修复方案,以及需要配套的网络隔离和监控措施。
整改优先级通常可以按以下顺序安排:先限制协议和目标范围,再处理 DNS、重定向和地址校验,随后补充超时与响应大小限制,最后完善出口网络策略、日志和回归测试。这样既能快速降低风险,也不会把问题简单归结为某个 API 的替换。
总结
SSRF 的核心不是某一个特殊 URL,而是“用户可控目标”和“服务端网络能力”之间缺少清晰边界。审计时应沿着输入、解析、DNS、连接和响应处理完整追踪;修复时应采用精确白名单、规范化解析、地址校验、重定向控制和网络出口隔离的组合方案。
对于确实需要远程资源访问的业务,安全目标不是完全禁止请求,而是让每一次请求都具备明确的协议、主机、端口、解析地址、资源大小和时间边界。把这些约束固化到统一下载组件中,再用本地隔离环境做覆盖性测试,通常比在各个业务接口里重复添加字符串判断更可靠,也更容易长期维护。

服务器、云资产与权限边界梳理示意
推荐意见