跳转到帖子

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 目标重新执行协议、域名、解析结果和端口校验。

如果业务不需要重定向,最简单的方案是直接关闭;如果必须支持,应限制为少量跳转,并拒绝跨协议和跨信任域的跳转。

四、一个可落地的防护流程

下面是一套适合封装为公共组件的处理顺序。重点是让策略集中管理,避免不同业务各自实现一套不完整的校验逻辑。

  1. 解析 URL,并拒绝缺失主机名、用户名信息、异常端口或不支持的协议。
  2. 对主机名进行规范化,统一大小写并移除不必要的尾部点。
  3. 解析所有 A 和 AAAA 记录,拒绝任何落入禁止地址集合的结果。
  4. 根据业务策略校验端口和目标域名。
  5. 建立连接前再次确认解析结果,避免校验与连接之间出现变化。
  6. 关闭自动重定向,或对每一跳重复完整校验。
  7. 限制连接超时、读取超时、响应大小和总下载时间。
  8. 只允许必要的响应类型,并对下载内容进行独立解析,不能仅依赖文件扩展名。
  9. 通过网络出口策略限制应用服务器访问内网管理面和不必要的服务。

伪代码可以表示为:

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 客户端、解析库或部署环境发生变化时及时发现风险。

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

0篇意见

推荐意见

没有意见。

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