SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可控 URL”这么简单。真正影响风险等级的,通常还包括请求发起位置、网络出口权限、DNS 解析行为、重定向处理、协议支持范围以及云环境中的基础设施配置。
本文以代码审计和本地验证为主线,整理一套适合实际项目的 SSRF 分析方法。示例均针对自建测试服务和本地网络,不涉及访问第三方系统、云平台敏感接口或绕过授权。
一、问题场景与审计目标
常见业务功能包括图片抓取、Webhook 回调、远程文件导入、PDF 转换、站点连通性检测和 URL 预览。这些功能往往需要服务端根据用户输入发起 HTTP 请求,因此形成了 SSRF 的潜在入口。
审计时不要只关注参数名是否叫 url。更重要的是确认以下数据流:
- 用户输入是否进入了 HTTP、HTTPS、FTP 或其他网络客户端。
- 请求是否经过了统一的 URL 校验组件,还是在业务代码中直接拼接。
- 是否允许自动跟随重定向,以及重定向后的地址是否重新校验。
- DNS 解析发生在校验之前还是连接之前,解析结果是否可能变化。
- 应用运行账户能够访问哪些内网网段和本机服务。
- 响应内容是否会被原样返回给用户,或被写入文件、模板和日志。
二、从调用链定位真实风险
以 Java 应用为例,下面这类代码通常值得重点关注:
String target = request.getParameter("target");
URL url = new URL(target);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
try (InputStream in = conn.getInputStream()) {
Files.copy(in, outputPath, StandardCopyOption.REPLACE_EXISTING);
}风险点不只在 new URL。这里还存在协议支持、DNS 解析、重定向、响应大小、文件类型和落盘路径等多个问题。即使额外增加了字符串黑名单,例如拒绝 localhost,也不能覆盖 IP 表示形式、解析结果变化和重定向等情况。
实际审计中建议沿着以下链路追踪:
- 入口:HTTP 参数、JSON 字段、消息队列任务或数据库中的可编辑字段。
- 转换:URL 解码、规范化、拼接基础 URL、补全协议和端口。
- 请求:HTTP 客户端、图片库、爬虫组件、命令执行封装或第三方 SDK。
- 输出:响应正文、状态码、文件写入结果、异常信息和异步任务日志。
如果请求是在异步任务中执行,还需要检查权限上下文是否被带入。前台用户可能只能创建一条任务,但后台任务账户却拥有更宽的网络访问范围,这种权限差异经常导致风险被低估。
三、URL 校验中最容易遗漏的细节
1. 协议与端口限制
业务不需要的协议应直接关闭,通常只保留 HTTPS,必要时再兼容 HTTP。不要把“解析成功”当作“允许访问”。协议、主机、端口和路径都应纳入明确的策略。
对于图片抓取等场景,可以采用固定端口策略,例如只允许 443;如果业务必须支持自定义端口,也应限制端口范围,并结合出口防火墙进行二次控制。
2. 主机名校验不能代替 IP 校验
基于主机名后缀的判断,例如允许以某个域名结尾,容易受到域名混淆和解析变化影响。更可靠的方式是:
- 先解析主机名的所有 A、AAAA 记录。
- 对每个解析结果进行规范化。
- 拒绝回环地址、链路本地地址、私有地址、保留地址、组播地址和未指定地址。
- 真正建立连接时,尽量使用经过校验的解析结果,避免校验结果与连接结果不一致。
需要注意 IPv4 和 IPv6 必须分别处理。某些库会把 IPv4 映射到 IPv6 地址空间,如果只判断传统 IPv4 私网段,可能出现遗漏。
3. 重定向必须逐跳校验
很多 HTTP 客户端默认跟随 301、302、303 或 307 重定向。初始地址通过校验,并不代表最终地址安全。正确做法是关闭自动重定向,在应用层限制跳转次数,并对每一个 Location 目标重新执行协议、域名、解析结果和端口校验。
如果业务不需要重定向,最简单的方案是直接关闭;如果必须支持,应限制为少量跳转,并拒绝跨协议和跨信任域的跳转。
四、一个可落地的防护流程
下面是一套适合封装为公共组件的处理顺序。重点是让策略集中管理,避免不同业务各自实现一套不完整的校验逻辑。
- 解析 URL,并拒绝缺失主机名、用户名信息、异常端口或不支持的协议。
- 对主机名进行规范化,统一大小写并移除不必要的尾部点。
- 解析所有 A 和 AAAA 记录,拒绝任何落入禁止地址集合的结果。
- 根据业务策略校验端口和目标域名。
- 建立连接前再次确认解析结果,避免校验与连接之间出现变化。
- 关闭自动重定向,或对每一跳重复完整校验。
- 限制连接超时、读取超时、响应大小和总下载时间。
- 只允许必要的响应类型,并对下载内容进行独立解析,不能仅依赖文件扩展名。
- 通过网络出口策略限制应用服务器访问内网管理面和不必要的服务。
伪代码可以表示为:
validateUrl(input):
url = parseStrict(input)
require url.scheme in {"https"}
require url.username is empty
require url.port in allowedPorts
addresses = resolveAll(url.host)
require addresses is not empty
for address in addresses:
require isPublicRoutable(address)
return NormalizedTarget(url, addresses)
fetch(target):
current = target
for hop in range(MAX_REDIRECTS + 1):
connection = connectUsingValidatedAddress(current)
response = readWithTimeoutAndSizeLimit(connection)
if not isRedirect(response):
return response
current = validateUrl(resolveLocation(current, response.location))
raise RedirectLimitExceeded这里的 isPublicRoutable 不应只使用一个简单的字符串匹配函数,而应调用经过测试的 IP 地址库,并覆盖 IPv4、IPv6、映射地址和保留地址范围。不同语言生态中的地址库实现并不完全一致,升级依赖后也应保留回归测试。
五、本地验证方法
在不接触真实内网的前提下,可以搭建一个本地测试服务验证校验逻辑。下面的服务只监听本机回环地址,用于观察应用是否发起了请求:
python3 -m http.server 18080 --bind 127.0.0.1然后在测试环境中提交一个指向本地服务的 URL,预期结果应是请求在校验阶段被拒绝,并且应用日志中能够明确记录拒绝原因,例如“目标地址属于回环网段”,而不是泛化为“请求失败”。
重定向场景可以在本地编写一个简单的测试接口,分别模拟:
- 允许域名跳转到另一个允许域名。
- 允许域名跳转到本机回环地址。
- HTTPS 跳转到 HTTP。
- 连续多次跳转超过上限。
测试重点不是证明某个地址能否访问,而是确认每一跳是否重新执行策略,并验证失败时不会泄露响应正文、内部地址或连接异常堆栈。
六、常见错误实现与修正建议
| 错误做法 | 问题 | 修正方向 |
|---|---|---|
| 只拦截 localhost 字符串 | 无法覆盖 IP、IPv6、DNS 解析和别名情况 | 解析后按地址类型判断,并限制出口网络 |
| 只校验初始 URL | 重定向可能改变最终目标 | 关闭自动跳转或逐跳校验 |
| 仅依赖域名白名单 | 域名解析结果可能变化,且可能解析到非预期地址 | 域名与解析 IP 双重校验 |
| 使用黑名单阻止少数私网地址 | 规则容易遗漏保留地址和 IPv6 范围 | 采用明确的允许策略和标准地址库 |
| 没有响应大小限制 | 可能导致内存、磁盘或线程资源耗尽 | 限制 Content-Length、实际读取字节数和总耗时 |
| 将响应直接交给解析器 | 可能引入 XML 外部实体、压缩炸弹或恶意文件解析风险 | 先做类型、大小和格式校验,再使用安全解析配置 |
七、日志、监控与回归测试
安全控制需要可观测,否则出现误报或业务兼容问题时很难定位。建议记录以下字段:
- 请求来源的业务功能和任务 ID。
- 经过规范化后的目标主机、端口和协议。
- 拒绝原因,例如协议不允许、地址属于保留网段或重定向超限。
- DNS 解析结果的摘要,而不是在日志中记录敏感响应内容。
- 请求耗时、响应大小、状态码和是否发生重定向。
日志中不应记录完整的认证信息、访问令牌和可能包含敏感参数的 URL。对于允许访问的外部站点,也可以采用哈希或脱敏后的主机标识,降低日志本身的泄露风险。
回归测试至少应覆盖普通公网域名、回环地址、私有地址、链路本地地址、IPv6 地址、带端口地址、大小写混合域名、尾部点域名、重定向和 DNS 多记录场景。测试应在 CI 中自动执行,防止后续更换 HTTP 客户端或升级依赖时重新引入问题。
八、架构层面的补充防护
应用层校验不是唯一边界。更稳妥的做法是将应用服务器放在受控的网络出口之后:
- 默认拒绝访问内部管理网段、数据库网段和主机管理接口。
- 仅开放业务确实需要的 DNS、HTTP 或 HTTPS 出口。
- 通过统一代理执行域名和地址策略,减少每个应用重复实现。
- 将高风险的远程抓取任务放入隔离的沙箱或独立工作节点。
- 限制任务账户权限,避免抓取服务同时拥有文件系统、容器管理或高权限云角色。
如果应用部署在云环境,还应把实例角色权限、网络安全组和元数据服务访问策略作为独立检查项。不能假设应用层校验永远正确,更不能让一个普通的 URL 抓取功能拥有与核心业务相同的基础设施权限。
总结
SSRF 的审计重点是完整还原“输入如何变成网络连接”,而不是寻找某个固定字符串。高质量的防护应同时覆盖协议、主机名、DNS 解析结果、地址类型、端口、重定向、超时、响应大小和网络出口。
在实践中,建议把 URL 校验封装成统一组件,配合本地测试服务、自动化回归用例和出口网络策略。这样既能减少业务代码中的重复实现,也能在 HTTP 客户端、解析库或部署环境发生变化时及时发现风险。
推荐意见