SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替攻击者发一次 HTTP 请求”这么简单。真正需要关注的是:应用允许用户影响哪些请求参数、服务端具有什么网络访问能力、请求链路是否会发生重定向,以及最终响应是否会回传给用户。
在实际审计中,SSRF 经常隐藏在图片抓取、Webhook、文档转换、URL 预览、远程文件导入和健康检查等正常功能里。很多修复方案只增加一个内网 IP 黑名单,短期内能够降低风险,但在 DNS 解析、IPv6、重定向和代理配置等环节仍可能留下缺口。
一、典型业务场景
下面是一段常见的“根据 URL 下载远程资源”的伪代码:
String url = request.getParameter("url");
byte[] data = httpClient.get(url);
return Response.ok(data).build();这段代码的问题不在于使用了某个特定 HTTP 客户端,而在于用户输入直接进入了出站请求。只要应用服务器能够访问某个目标地址,攻击者就可能借助该功能探测网络边界、访问内部服务,甚至触发内部接口执行高风险操作。
常见的受影响功能包括:
- 头像、图片或附件的远程导入。
- Markdown、富文本或网页内容预览。
- Webhook 连通性检测。
- 在线 PDF、Office 或网页转码。
- 根据 URL 生成二维码、缩略图或摘要。
- 监控系统中的自定义探针。
二、审计时应先回答的几个问题
审计 SSRF 时,不要一开始就围绕某个绕过技巧展开。更有效的方式是先还原完整请求链路。
- 用户是否能够控制协议、主机、端口、路径或请求头。
- URL 是在入口处解析,还是交由底层客户端再次解析。
- 域名解析发生几次,解析结果是否会被缓存。
- HTTP 客户端是否自动跟随 30x 重定向。
- 是否配置了系统代理、环境变量代理或自定义 DNS。
- 响应内容、状态码、响应头或错误信息是否会返回给用户。
- 服务端所在网络是否能够访问管理网、容器网络或云平台保留地址。
从代码搜索角度,可以优先关注以下调用点:
URL.openConnection()
HttpClient.send()
RestTemplate.exchange()
WebClient.retrieve()
requests.get()
urllib.request.urlopen()
curl_exec()同时要追踪参数来源,尤其是控制器参数、JSON 字段、消息队列任务和数据库中可编辑的配置。很多 SSRF 并不直接位于 HTTP 接口,而是由异步任务在后台执行。
三、风险形成的关键环节
1. 协议校验不足
如果业务只需要访问 HTTPS 资源,就不应接受任意协议。协议白名单应当在 URL 解析后进行,而不是简单地使用字符串前缀判断。
URI target = URI.create(input);
String scheme = target.getScheme();
if (!"https".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("unsupported scheme");
}仅检查字符串是否以“ URL 语法中还存在用户名、端口、编码和解析差异等情况。安全判断应基于标准 URI 解析结果。
2. 域名解析与实际连接目标不一致
一个常见问题是:应用先解析域名并检查得到的 IP,然后 HTTP 客户端在真正建立连接时再次解析域名。两次解析之间如果结果发生变化,前一次检查就可能失效。
因此,不能把“解析域名并判断 IP”与“后续发起连接”视为两个互不相关的步骤。较稳妥的实现需要让校验结果参与最终连接,或者使用受控 DNS、固定解析结果和连接器层面的目标校验。
在 Java 应用中,建议将域名解析、地址过滤和连接建立封装为统一组件,避免各处自行调用底层客户端。例如:
ResolvedTarget target = resolver.resolveAndValidate(uri.getHost());
if (target.isPrivateAddress() || target.isLoopback() || target.isLinkLocal()) {
throw new SecurityException("blocked destination");
}
httpClient.get(target.getConnectAddress(), uri.getPath());这里的重点不是某个类名,而是校验和连接使用同一份解析结果,并且在连接阶段再次确认目标地址。
3. 自动重定向扩大了请求范围
即使初始 URL 是一个合法的公网地址,目标服务器也可能返回重定向,把请求引向应用原本不允许访问的地址。因此,远程抓取功能默认不应自动跟随重定向。
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(5))
.build();如果业务确实需要重定向,应对每一跳重新执行协议、主机、端口和 IP 校验,并限制最大跳转次数。不能只检查第一跳。
4. IPv4 规则不能代替完整地址策略
很多项目只过滤常见的 IPv4 内网网段,却遗漏了回环地址、链路本地地址、IPv6 本地地址、IPv4 映射地址以及应用运行环境中的特殊网段。
建议使用成熟的 IP 地址库判断地址属性,而不是维护一组简单字符串规则。至少应明确处理以下类别:
| 类别 | 示例 | 处理建议 |
| 回环地址 | IPv4 和 IPv6 loopback | 默认拒绝 |
| 私有地址 | RFC 1918 网段 | 默认拒绝或按业务放行 |
| 链路本地地址 | IPv4、IPv6 link-local | 默认拒绝 |
| 未指定地址 | 0.0.0.0、IPv6 unspecified | 拒绝 |
| 云平台保留地址 | 由云环境定义的元数据地址 | 结合网络策略阻断 |
| 容器与编排网络 | 容器网段、服务网段 | 按网络边界单独控制 |
具体网段会因云厂商、容器平台和企业网络架构不同而变化,因此代码规则不能替代网络层隔离。
四、推荐的安全校验流程
一个相对清晰的远程请求流程可以拆分为以下步骤:
- 限制输入长度,拒绝控制字符和异常编码。
- 使用标准库解析 URL,不接受解析失败的输入。
- 仅允许业务必需的协议。
- 限制端口,避免开放到任意管理端口。
- 解析主机名并检查全部结果,而不是只检查第一个地址。
- 拒绝回环、私有、链路本地、未指定和业务不需要的保留地址。
- 禁用自动重定向,或对每次重定向重复校验。
- 设置连接超时、读取超时、响应大小和总耗时上限。
- 限制响应类型,避免把远程内容当作脚本、模板或本地文件处理。
- 记录目标域名、解析地址、端口、状态码和拦截原因。
下面给出一个偏防御性的 Python 示例,适合说明流程。生产环境中仍应结合成熟网络库和统一出口策略:
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {"https"}
ALLOWED_PORTS = {443}
def validate_target(raw_url):
if len(raw_url) > 2048:
raise ValueError("url too long")
parsed = urlparse(raw_url)
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
raise ValueError("scheme denied")
if not parsed.hostname:
raise ValueError("host missing")
port = parsed.port or 443
if port not in ALLOWED_PORTS:
raise ValueError("port denied")
addresses = socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM)
for item in addresses:
address = ipaddress.ip_address(item[4][0])
if (address.is_private or address.is_loopback or
address.is_link_local or address.is_unspecified or
address.is_reserved):
raise ValueError("address denied")
return parsed这个示例有两个重要限制:第一,DNS 校验与真正连接之间仍可能存在时间窗口;第二,标准属性判断不能自动覆盖所有企业网络和云平台场景。因此更推荐通过受控代理或网络出口服务统一执行目标校验。
五、比代码黑名单更可靠的架构控制
SSRF 防护不应只依赖应用层判断。对于确实需要访问外部资源的服务,可以采用“应用白名单加网络出口控制”的组合方案。
- 应用只允许访问明确的域名或域名后缀,避免任意 URL。
- 出站流量统一经过 egress proxy,由代理执行域名、IP、端口和审计策略。
- 应用运行账户不具备访问管理网和敏感服务网段的路由能力。
- 容器或虚拟机使用独立安全组,默认拒绝不必要的出站连接。
- 将远程抓取服务与主业务服务隔离,降低凭据和内网权限暴露面。
- 禁止从用户可控请求中继承代理认证信息、内部请求头和云环境凭据。
如果业务只允许从固定对象存储或图片 CDN 导入,最好的方案通常不是实现一个“通用 URL 下载器”,而是维护可信域名清单,并在网络层进一步限制访问范围。
六、验证与测试方法
测试应在授权的隔离环境中进行,重点是确认拦截逻辑和网络边界,而不是访问真实生产内部服务。可以搭建一个本地测试服务,分别返回正常响应、重定向响应和超大响应,用来验证客户端行为。
python3 -m http.server 8080 --bind 127.0.0.1在测试环境中重点观察以下情况:
- 非法协议是否在发起网络连接前被拒绝。
- 回环地址和测试网段是否被拦截。
- 域名解析得到多个地址时是否全部校验。
- 重定向到不允许目标时是否停止。
- 连接超时和读取超时是否生效。
- 响应体超过限制时是否及时中断。
- 错误信息是否泄露内部地址、DNS 结果或认证头。
建议把这些场景写成自动化测试,而不是只进行一次人工验证。安全规则一旦被后续重构覆盖,自动化测试可以较早发现回归问题。
七、日志与监控中的实用字段
SSRF 防护通常需要结合日志判断是否存在异常探测。日志不必记录完整响应内容,但应保留足够的决策信息:
- 请求来源、业务用户和请求链路 ID。
- 原始域名的脱敏值以及规范化后的主机名。
- 最终解析地址、端口和协议。
- 是否发生重定向以及每一跳的决策结果。
- 拦截原因,例如协议不允许、地址属于私有网段或超出大小限制。
- 连接耗时、响应状态码和响应字节数。
如果短时间内出现大量不同端口、连续失败的内网地址或异常 DNS 请求,应结合网关、DNS 和主机网络日志进行关联分析。单独依赖应用日志,往往只能看到请求参数,无法还原实际网络行为。
八、常见错误修复方式
- 只判断 URL 是否以某个字符串开头,没有进行标准解析。
- 只过滤少量 IPv4 私有网段,忽略 IPv6 和链路本地地址。
- 在校验阶段解析一次,连接阶段又由客户端重新解析。
- 校验初始 URL,却允许客户端无条件跟随重定向。
- 只限制响应状态码,不限制响应大小和读取时间。
- 把错误堆栈直接返回前端,暴露内部主机名和网络结构。
- 应用层做了过滤,但服务器仍能直接访问整个内网。
- 使用“任意 URL 下载”满足需求,却没有评估是否可以改成可信源白名单。
九、审计结论的判定思路
发现一个可控 URL 参数并不等于已经确认高危 SSRF。最终风险需要结合出站网络权限、响应可见性和实际业务影响判断。
| 条件 | 风险影响 |
| 只能访问固定公网域名 | 通常属于较低风险,但仍需防止重定向和资源消耗 |
| 可访问任意公网地址且响应回显 | 可能造成内网探测和敏感信息读取 |
| 可访问内部管理服务 | 需要结合认证、请求方法和内部接口权限进一步评估 |
| 可访问云环境敏感保留地址 | 应优先通过网络层阻断并检查凭据暴露风险 |
| 只能触发请求但无响应回显 | 仍可能造成盲 SSRF、内部探测和内部服务副作用 |
十、总结
SSRF 的核心问题是用户输入跨越了信任边界,进入了服务端的网络访问能力。有效防护不能依赖一个简单黑名单,而应同时覆盖 URL 解析、DNS、连接、重定向、响应处理和网络出口。
从工程实践看,优先级可以这样安排:先限制业务需求和协议范围,再实现统一的目标校验;随后关闭不必要的重定向和代理继承,补充超时与响应大小限制;最后通过 egress 代理、安全组和服务隔离建立第二道边界。
对于审计人员而言,最有价值的不是找到一个参数,而是完整回答“输入如何到达连接层、连接最终去了哪里、响应返回了什么、网络边界能阻止什么”这四个问题。只有把代码行为和真实网络拓扑结合起来,才能准确判断漏洞影响并给出可落地的修复建议。
推荐意见