跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不是单纯的“请求了一个危险 IP”问题。实际审计中,风险往往来自 URL 解析不一致、重定向未限制、DNS 解析时机不当,以及应用所在网络缺少出口控制。本文从代码审计、授权验证和事件复盘三个角度,整理一套可落地的 SSRF 排查与防护方法。

一、问题背景:为什么 SSRF 容易被低估

常见业务功能包括头像抓取、Webhook、在线导入、网页截图、PDF 转换、远程文件解析和第三方 API 代理。这些功能都需要服务端根据用户输入访问一个 URL,本身并不等于存在漏洞,但如果访问目标、协议和网络出口没有经过严格约束,就可能形成 SSRF。

SSRF 的影响取决于应用权限和网络位置,常见风险包括:

  • 访问云环境元数据服务、内部管理接口或仅监听在内网地址的服务。
  • 通过服务端网络身份访问内部 API,造成越权读取或配置变更。
  • 探测内网端口和服务类型,扩大后续攻击面。
  • 利用高权限服务访问本地文件、Unix Socket 或特殊协议处理器。
  • 将服务变成代理,影响审计溯源并增加出口带宽和资源消耗。

在实际风险评估中,不能只看“是否能访问内网”,还要结合服务账号权限、网络分区、响应内容是否回显、请求是否支持写操作等因素判断影响。

二、代码审计:先找出“用户可控 URL”数据流

审计时建议从数据流而不是函数名入手。先定位用户输入,再沿着参数传播到 HTTP 客户端、文件读取器、浏览器自动化组件或命令执行封装的位置。

重点搜索以下类型的调用:

  • HTTP 客户端:requests、urllib、OkHttp、RestTemplate、WebClient、axios 服务端封装等。
  • 文件和资源读取:URL.openStream、file_get_contents、fopen、ImageIO、PDF 解析器。
  • 浏览器和渲染组件:Playwright、Selenium、Puppeteer、wkhtmltopdf。
  • 业务代理接口:根据 path、url、target、callback、webhook 等参数转发请求的控制器。
  • 异步任务:队列消费者、定时任务或媒体处理服务中的远程资源下载逻辑。

一个常见的危险模式如下。问题不一定在 HTTP 库本身,而在于目标地址完全由请求参数决定,且没有统一的策略层。

url = request.form.get("url")
response = http_client.get(url, timeout=10, allow_redirects=True)
return response.content

审计时应继续追踪以下问题:是否允许重定向、是否支持非 HTTP 协议、是否验证最终地址、是否解析响应体、是否将响应原样返回、是否复用了带内部凭据的客户端、是否允许长连接或大响应。

三、关键技术点:为什么简单黑名单经常失效

1. 解析与校验使用了不同规则

如果校验模块和真正发起请求的 HTTP 客户端使用不同的 URL 解析器,就可能出现“校验通过、实际访问另一个地址”的问题。尤其要关注用户名密码、端口、片段、编码字符和异常斜杠等字段对解析结果的影响。

推荐的原则是:只允许明确需要的 scheme 和端口;使用同一个标准解析器完成规范化、主机提取和后续请求;禁止将未经规范化的原始字符串直接交给网络客户端。

2. DNS 解析与请求之间存在时间差

只在校验阶段解析域名一次,然后把域名交给 HTTP 客户端重新解析,会形成 DNS rebinding 风险。即使校验时解析到公网地址,真正建立连接时也可能得到另一组结果。

防护上不能只依赖应用层的一次 DNS 查询。更稳妥的做法是:

  • 在受控解析器或出口代理中统一解析并执行目标策略。
  • 对解析得到的所有地址进行检查,而不是只检查第一个地址。
  • 连接建立前再次确认最终地址,必要时固定经过策略代理访问。
  • 对域名解析结果设置合理缓存,并监控短时间内频繁变化的解析记录。

3. IPv4、IPv6 和不同数字表示法

仅判断字符串是否以某个内网前缀开头是不可靠的。IPv6 回环地址、IPv4-mapped IPv6 地址、私有地址、链路本地地址,以及十进制或其他形式表示的 IPv4,都可能绕过简单字符串规则。

校验时应先把主机解析成 IP 对象,再使用标准库判断是否为回环、私有、链路本地、未指定或保留地址。下面是一个防御性示例,重点是“解析全部结果并逐个判断”,并不代表可以替代网络出口控制。

from ipaddress import ip_address, IPv4Address, IPv6Address
import socket
from urllib.parse import urlparse

BLOCKED_SCHEMES = {"file", "gopher", "ftp", "data"}
ALLOWED_SCHEMES = {"http", "https"}


def resolve_and_validate(url: str):
    parsed = urlparse(url)
    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        raise ValueError("scheme is not allowed")
    if not parsed.hostname:
        raise ValueError("hostname is required")
    if parsed.username or parsed.password:
        raise ValueError("userinfo is not allowed")

    host = parsed.hostname
    port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
    results = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
    addresses = {item[4][0] for item in results}

    for raw in addresses:
        addr = ip_address(raw)
        if (addr.is_private or addr.is_loopback or addr.is_link_local
                or addr.is_reserved or addr.is_unspecified):
            raise ValueError("destination address is not allowed")

    return parsed, addresses

这段代码只展示审计和防御思路。生产环境还需要处理解析失败、连接复用、代理行为、IPv6 出口策略、重定向以及证书校验等问题,不能直接当作完整的安全组件复制使用。

4. 重定向会改变最终目标

初始 URL 是公网地址,并不代表最终请求目标安全。服务端如果自动跟随 30x 响应,就必须对每一次跳转后的地址重新执行 scheme、主机、端口和 IP 策略检查。

安全设计通常有两种方式:关闭自动重定向,由业务明确处理;或者限制最大跳转次数,并在每一跳都重新进行完整校验。禁止跨协议跳转,禁止跳转到未允许的端口,并记录初始地址和最终地址,便于审计。

四、建议的审计与验证流程

第一步:建立资产和调用清单

列出所有能够访问外部资源的服务、接口和后台任务,记录调用方、运行账号、网络区域、是否经过代理、是否携带认证头,以及响应是否回显给用户。对于图片处理、文档转换和浏览器渲染服务,应单独评估,因为它们通常具备更复杂的协议和解析能力。

第二步:确认输入到网络请求的完整链路

不要只审查控制器入口。需要继续检查参数是否经过二次拼接、是否在消息队列中延迟处理、是否由其他微服务再次解析,以及是否存在不同语言实现的重复校验。多服务场景下,最容易出现“入口校验过,内部服务又直接请求”的断点。

第三步:使用受控测试地址验证行为

授权测试时,优先使用组织自有的回显服务或本地测试环境,确认以下行为:

  • 是否发起了 DNS 解析和 TCP 连接。
  • 是否自动跟随重定向。
  • 是否支持非 HTTP 协议。
  • 是否携带内部认证头、Cookie 或代理凭据。
  • 是否限制响应大小、连接时间和读取时间。
  • 是否将错误信息、响应头和响应体直接返回。

验证重点是确认控制逻辑,而不是访问真实的云元数据、内部管理面板或第三方系统。测试记录应包括时间、请求 ID、测试地址、服务版本和出口日志,便于复现和修复验证。

第四步:检查网络侧是否有第二道防线

应用层校验存在实现缺陷的可能,网络出口控制应作为独立防线。建议将需要访问互联网的服务放入单独网络区域,通过 egress proxy 或防火墙只允许必要的目标域名和端口;禁止直接访问云平台元数据地址、管理网段、容器编排控制面和内部 DNS 管理接口。

如果业务只需要访问少量固定供应商,优先使用域名白名单加出口代理,而不是允许任意互联网地址。白名单规则还应明确是否允许其所有子域名、是否允许跨区域跳转,以及供应商地址变化时如何变更。

五、常见错误方案及改进建议

错误方案主要问题改进方向
只拦截 127.0.0.1遗漏 IPv6、私有网段、链路本地地址和云内部地址解析为 IP 对象并统一判断,配合出口网络策略
只校验初始 URL重定向后可能进入受限网络关闭重定向或逐跳重新校验
只做域名后缀白名单子域名接管、DNS 变化和解析时差可能绕过规则使用可信解析链路、固定出口和明确的域名边界
直接使用用户提供的 Host 头可能造成内部路由混淆和日志污染目标主机与 Host 头分别规范化,禁止用户覆盖关键头
仅依赖应用代码防护遗漏其他服务或新上线接口时容易失守增加 egress 防火墙、代理策略和监控告警

六、事件复盘时应重点关注什么

如果已经发现疑似 SSRF 事件,首先不要只删除接口或修改黑名单。应保留网关、应用、DNS、代理、防火墙和云平台审计日志,按请求 ID 或时间窗口还原完整链路。

建议重点确认:

  • 请求参数、初始 URL、每次重定向地址和最终解析 IP。
  • 服务是否携带了云角色凭据、内部 Cookie 或高权限请求头。
  • 是否存在对内网服务的写请求、配置变更或敏感数据读取。
  • 同一来源是否对多个网段、端口或服务进行了系统性探测。
  • 是否有异常 DNS 解析、短时间大量失败连接或出口流量突增。

处置上,应先收紧出口访问策略并轮换可能暴露的临时凭据,再修复应用校验逻辑。对于云环境,应确认元数据服务是否启用了更严格的访问模式,并限制工作负载获取不必要的实例权限。修复完成后,需要用原始请求链路回归验证,而不是只验证一个被阻断的 IP。

七、工程化落地建议

  • 把远程资源访问封装成统一组件,禁止业务代码直接调用底层 HTTP 客户端访问用户输入地址。
  • 将目标校验、重定向控制、超时、响应大小、协议限制和日志记录集中实现。
  • 对 URL 访问设置连接超时、读取超时、最大响应体、最大重定向次数和并发上限。
  • 默认不发送 Cookie、Authorization、内部 Trace 头等敏感信息。
  • 对图片、文档和浏览器渲染服务采用独立低权限账号和隔离网络,避免与核心业务共享凭据。
  • 在 CI 中加入静态规则,重点检测用户输入是否直接流入网络请求、文件读取和浏览器导航函数。
  • 在网关和出口代理记录结构化日志,包括目标域名、解析地址、策略结果、响应状态和耗时。

总结

SSRF 防护的核心不是维护一份不断增长的黑名单,而是建立完整的目标访问控制链:规范化 URL、限制协议和端口、解析并校验所有地址、控制重定向、避免 DNS 时序问题,再用出口代理和网络隔离提供独立防线。

代码审计时,最有价值的不是找到一个看起来危险的请求调用,而是还原从输入到最终连接的完整数据流,并结合服务权限、网络位置和日志证据判断实际影响。对于已经上线的系统,统一访问组件、最小网络权限和可追溯日志,通常比单点修补更能降低长期风险。

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

0篇意见

推荐意见

没有意见。

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