跳转到帖子

SSRF(服务端请求伪造)并不是“让服务器访问一个 URL”这么简单。真正容易出问题的地方,通常在于业务允许用户控制请求目标,却没有明确限制协议、目标地址、解析结果和重定向行为。审计时如果只搜索 HttpClient、RestTemplate 或 requests.get,往往会漏掉藏在图片抓取、Webhook、文件导入、链接预览和第三方回调中的风险。

一、问题场景与风险边界

常见业务流程如下:

  • 用户提交一个图片或文档 URL,服务端负责下载并解析。
  • 平台提供 Webhook,服务端向用户配置的地址发送事件通知。
  • Markdown、富文本或链接预览功能由后端抓取页面标题和摘要。
  • 管理后台支持导入远程配置、远程头像或第三方接口数据。

当请求目标完全由外部输入控制时,风险不只包括访问内网服务,还可能涉及内部管理接口、云环境凭据服务、服务发现接口和本机管理端口。实际影响取决于网络隔离、服务权限、响应内容是否回显以及是否允许携带认证信息。

审计和验证应在授权环境或本地靶场中进行。不要把生产内网、云元数据地址或第三方系统作为测试目标。

二、代码审计:从输入点追踪到请求出口

建议将审计分为“入口、变换、出口、响应处理”四个部分,而不是只盯着 HTTP 客户端调用。

  • 入口:查找 query、JSON、表单、文件元数据和配置项中的 URL 字段。
  • 变换:关注 URL 拼接、Base64 解码、模板替换、重定向参数和二次解析。
  • 出口:检查 HTTP、HTTPS、FTP、文件协议、Unix Socket 或自定义网络客户端。
  • 响应处理:判断响应是否被直接返回、写入日志、解析 XML、转换图片或保存到对象存储。

以下代码是一个典型的危险模式:

String target = request.getParameter("url");
URL url = new URL(target);
URLConnection connection = url.openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(5000);
return connection.getInputStream().readAllBytes();

这里的问题不只是缺少域名白名单。URL 允许多种协议,连接过程可能发生 DNS 解析和重定向,读取响应时也没有限制大小,更没有确认最终连接地址是否仍然满足安全策略。

三、验证思路:使用本地可控环境复现

在不触碰真实内网的前提下,可以准备两个本地服务:一个模拟业务接口,另一个模拟“内部服务”。业务接口接收 URL 并执行抓取,内部服务仅返回固定的测试文本。这样可以验证是否存在服务端请求行为、是否允许非预期端口、是否跟随重定向,以及响应是否会回显。

# 模拟一个仅用于测试的 HTTP 服务
python3 -m http.server 18080 --bind 127.0.0.1

测试时只访问本机靶场地址,并记录以下结果:

  • 是否接受 http 以外的协议。
  • 是否能够访问回环地址、链路本地地址或私有网段。
  • DNS 解析到受限地址时,是否仍然建立连接。
  • 服务端是否自动跟随 30x 重定向。
  • 响应体是否原样返回给调用方。
  • 超时、响应长度和并发数量是否受到限制。

验证重点是确认安全边界,而不是扩大访问范围。对于只需要确认“服务端是否发起请求”的场景,使用本地日志或一次性测试标记即可,不需要读取任何敏感服务内容。

四、常见绕过点与审计注意事项

1. 只校验字符串前缀

类似 target.startsWith(" 的判断不可靠。攻击者可能利用用户名密码段、端口、路径或相似域名改变实际解析结果。应先使用标准 URL 解析器拆分协议、主机、端口和路径,再对结构化字段做校验。

2. 校验域名但不校验解析地址

域名可能解析到回环地址、私有地址或链路本地地址。生产实现通常需要在连接前解析主机名,拒绝非公网地址;同时还要考虑 DNS 解析结果变化,避免“校验时是安全地址、连接时已变更”的竞态问题。对于高风险场景,应由统一网络代理负责解析和出站控制,而不是让每个业务自行实现。

3. 忽略重定向

初始 URL 可能属于允许域名,但响应通过 30x 跳转到不允许的目标。最简单的防护是关闭自动重定向;如果业务必须支持重定向,则应对每一跳重新执行协议、域名、端口和 IP 校验,并限制跳转次数。

4. 解析器与连接器行为不一致

应用层解析 URL 使用的库,和底层 HTTP 客户端实际连接的地址可能存在差异。审计时不能只看校验函数,还要确认最终请求库是否支持代理、是否自动补全端口、是否允许 IPv6,以及是否会重新解析主机名。

5. 只限制响应内容,不限制网络行为

将响应体截断到几 MB 只能降低数据泄露影响,不能阻止端口探测、连接耗尽或对内部服务造成压力。因此需要同时设置连接超时、读取超时、响应大小、单请求跳转次数、并发数和出站网络策略。

五、一个更稳妥的防护实现思路

在业务层,建议采用“默认拒绝、明确允许”的策略。下面是伪代码,重点展示校验顺序,实际项目应结合统一 HTTP 客户端和网络出口策略实现:

URI target = URI.create(input);

if (!"https".equalsIgnoreCase(target.getScheme())) {
    throw new SecurityException("unsupported scheme");
}
if (target.getUserInfo() != null || target.getPort() == -1) {
    throw new SecurityException("invalid authority");
}

String host = target.getHost();
if (host == null || !allowlistedHost(host)) {
    throw new SecurityException("host not allowed");
}

List<InetAddress> addresses = resolve(host);
for (InetAddress address : addresses) {
    if (isLoopback(address) || isPrivate(address) || isLinkLocal(address)) {
        throw new SecurityException("address not allowed");
    }
}

// 禁止自动重定向;如业务必须支持,必须对每一跳重复上述校验
HttpResponse response = client.get(target)
    .withConnectTimeout(Duration.ofSeconds(3))
    .withReadTimeout(Duration.ofSeconds(5))
    .withMaxBodyBytes(2 * 1024 * 1024)
    .execute();

更推荐将出站请求集中到一个受控服务中:业务服务只提交经过规范化的目标信息,出口服务负责协议限制、域名白名单、DNS 校验、代理访问、审计日志和速率控制。这样可以减少不同团队重复实现安全逻辑造成的差异。

六、日志、监控与事件复盘

安全日志至少应记录请求发起方、业务功能、规范化后的目标域名、目标端口、解析地址、是否发生重定向、响应状态、耗时和失败原因。不要记录完整的认证头、敏感查询参数或响应正文。

事件复盘时可以按以下顺序梳理:

  • 最初的输入点是什么,是否存在多个入口。
  • 请求经过了哪些解析、代理和重定向环节。
  • 出站网络是否允许访问私有地址和管理端口。
  • 响应是否被回显、缓存、写入对象存储或进入日志平台。
  • 是否存在凭据泄露、横向访问或服务可用性影响。
  • 修复后是否补充了单元测试、集成测试和网络侧阻断策略。

测试用例不要只覆盖一个恶意 URL。至少应包含协议不匹配、缺少端口、带用户信息、IPv4 回环、IPv6 回环、私有网段、域名解析到受限地址、跨域重定向、超大响应和连接超时等情况。

七、总结

SSRF 防护的核心不是增加一条字符串黑名单,而是建立完整的出站请求安全边界:限制协议,规范化解析,校验域名和解析地址,控制重定向,限制资源消耗,并在网络层实施默认拒绝。代码审计时应沿着“输入—变换—出口—响应”追踪完整链路;验证时使用本地可控服务确认行为;修复后再通过日志和测试用例验证策略是否真正生效。

服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…

SiteMap HACKHAT © 2026 All rights reserved.