SSRF(服务端请求伪造)并不是只存在于“图片抓取”或“Webhook”功能中。凡是后端接收用户提供的 URL,并由服务器代为发起网络请求的场景,都可能引入类似风险。实际审计中,真正棘手的地方往往不是发现一个可以请求外部站点的参数,而是判断请求链路、网络边界、重定向行为和 DNS 解析是否形成了可被滥用的组合。
本文以一个常见的 URL 预览功能为例,整理一次偏防守视角的审计过程:如何定位请求入口,如何在不触碰敏感内部资源的前提下完成验证,如何分析绕过风险,以及修复后如何通过测试和日志确认问题已经闭环。
一、场景与问题边界
某业务提供“输入网页地址,生成标题、摘要和缩略图”的功能。前端提交一个 URL,后端通过 HTTP 客户端访问目标页面,再解析返回内容。
POST /api/preview
Content-Type: application/json
{"url":" 解析或代理配置不当导致的访问边界绕过。需要注意的是,SSRF 不等于“服务器能访问任意外网地址”。有些实现只允许固定协议,但仍然可能访问内网;有些实现禁止私网 IP,却没有处理重定向和 DNS 重新解析;还有些实现本身无法读取响应内容,但仍然会对内部系统产生状态变化。
二、审计入口:先还原完整请求链路
审计时不建议一开始就围绕某个参数尝试各种 URL,而应先确认从输入到网络请求之间经过了哪些组件。常见链路如下:
客户端输入 URL
-> 参数校验
-> URL 解析与规范化
-> DNS 解析
-> HTTP 客户端建立连接
-> 处理重定向
-> 读取响应
-> HTML 解析或图片下载
-> 缓存、落盘或异步任务
可以从代码搜索和运行时观测两条线并行进行:
- 搜索
requests.get、http.Client.Do、WebClient、curl、libcurl、浏览器渲染器等网络请求调用点。 - 沿调用链向上追踪 URL 的来源,确认是否来自用户输入、数据库字段、消息队列或第三方回调。
- 检查是否存在统一的 HTTP 客户端封装,以及不同业务是否绕过了该封装。
- 确认是否自动跟随 3xx 重定向,是否支持 HTTP、HTTPS 之外的协议。
- 确认响应内容最终会进入哪里,例如 HTML 解析器、图片处理库、文件系统或缓存。
如果只有黑盒测试条件,可以使用自己控制的测试站点记录请求方法、请求头、来源地址、重定向行为和 DNS 查询情况。验证目标应当是确认请求确实由服务端发出,而不是直接测试敏感内网地址。
三、验证思路:用可控目标证明请求确实发生
安全验证应优先使用低风险、可回收的测试资源。例如准备一个仅返回固定文本的测试地址,并记录访问时间、User-Agent 和请求路径。提交该地址后,如果服务端日志或测试站点能够观察到请求,即可确认存在服务端代发请求行为。
验证 SSRF 的第一步是证明“请求由服务端发起”,不是证明“可以访问某个敏感地址”。在授权测试中,尽量不要探测生产环境内网端口,也不要读取与业务无关的内部响应。
如果功能会跟随重定向,可以在测试站点上配置一个从公网地址跳转到另一个由测试人员控制的地址,用于确认客户端是否自动跟随重定向。整个过程仍然只使用外部可控资源,不需要访问内部系统。
对于“只发请求、不返回响应内容”的场景,可以观察以下信号:
- 测试站点是否收到了请求。
- 应用响应时间是否与目标站点延迟相关。
- 后台任务状态或错误信息是否发生变化。
- 服务端是否产生 DNS 查询、连接超时或证书校验错误。
四、重点分析一:URL 解析不能只看字符串前缀
常见但不可靠的修复方式,是判断 URL 是否以 开头,或者直接使用字符串匹配禁止某些主机名。URL 解析涉及用户名、端口、IPv4、IPv6、编码、大小写和重定向等多个因素,字符串判断很容易与实际连接目标不一致。
例如,下面这些输入在解析时可能产生与直觉不同的结果:
- 带有用户名和密码部分的 URL,真正的主机位于
@ 之后。 - 使用十进制、八进制或十六进制表示的 IPv4 地址。
- 使用 IPv6 地址以及压缩表示法。
- 主机名末尾带点、大小写混合或包含国际化域名。
- 通过 3xx 响应跳转到另一个协议或主机。
因此,建议使用成熟 URL 解析库,明确拆分协议、主机、端口、用户名和路径,并对解析结果再次规范化。业务如果只需要访问少量合作站点,优先使用域名白名单,而不是维护一份不断扩大的“危险地址黑名单”。
五、重点分析二:DNS 解析与连接目标必须保持一致
SSRF 防护中经常被忽略的一点是 DNS 重绑定。一个域名在校验阶段解析到公网地址,实际建立连接时却解析到了内网地址,或者解析结果在多个 IP 之间变化,都可能造成校验结果与连接目标不一致。
防守侧至少需要考虑以下原则:
- 校验协议和主机名后,解析所有 A、AAAA 记录。
- 拒绝解析结果中的回环地址、私有地址、链路本地地址、保留地址和多播地址。
- 建立连接时使用已经校验过的解析结果,避免校验和连接之间再次进行不受控解析。
- 如果必须支持域名重解析,要对每次连接重新执行完整的地址校验。
- 在网络层使用出口防火墙或代理策略作为第二道边界,不能只依赖应用层判断。
下面是用于说明思路的伪代码。实际项目应结合语言运行时和 HTTP 客户端能力实现,不能直接把示例当作完整安全组件使用:
parsed = parse_url(input)
if parsed.scheme not in ["http", "https"]:
reject()
if parsed.userinfo is not empty:
reject()
addresses = resolve_all(parsed.hostname)
if addresses is empty:
reject()
for address in addresses:
if is_loopback(address) or is_private(address):
reject()
if is_link_local(address) or is_reserved(address):
reject()
# 连接时固定到已经校验过的地址,或让受控代理完成连接
response = http_client.fetch(
scheme=parsed.scheme,
host_header=parsed.hostname,
port=parsed.port,
path=parsed.path,
resolved_address=selected_validated_address,
follow_redirects=false,
timeout=3s
)
这里最容易出现的实现错误,是校验时调用一次 DNS 解析,随后把原始域名交给另一个 HTTP 库,而该库又自行解析了一次。这样前面的校验可能失去意义。
六、重点分析三:重定向、协议和代理配置
即使初始 URL 通过了校验,重定向也可能把请求带到不允许的目标。因此,修复时不能只校验第一跳。
- 默认关闭自动重定向;如果业务必须支持,则对每一跳重新执行协议、域名、解析地址和端口校验。
- 限制最大重定向次数,避免循环跳转和资源消耗。
- 只允许业务真正需要的协议,通常是 HTTP 和 HTTPS。
- 检查 HTTP 客户端是否支持代理环境变量,避免生产环境中的
HTTP_PROXY、HTTPS_PROXY 或 NO_PROXY 配置改变预期网络路径。 - 限制端口范围。若业务只访问标准 Web 服务,可以只允许 80 和 443。
如果服务运行在容器或云环境中,HTTP 客户端还可能受到 sidecar、透明代理、服务网格和出口网关影响。应用层认为请求发往公网,并不代表实际链路一定如此,需要结合网络策略和基础设施配置进行确认。
七、响应处理同样需要限制
SSRF 的风险不仅在于能否访问某个地址,也在于服务端如何处理返回内容。URL 预览功能常见的后续问题包括:
- 无限制读取大响应,导致内存或磁盘资源耗尽。
- 自动解压压缩内容,产生压缩炸弹风险。
- 把响应内容交给 HTML、XML、图片或文档解析器,扩大攻击面。
- 将远端响应持久化到缓存,后续被其他用户访问。
- 错误信息中泄露内部解析地址、请求头或网络拓扑。
建议在 HTTP 层设置连接超时、读取超时、最大响应体大小和最大响应头大小,并在解析前先检查内容类型。对于缩略图一类功能,还应限制图片尺寸、解码格式和处理时间,避免把 SSRF 问题扩大成解析器拒绝服务。
client := http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return errors.New("too many redirects")
}
return validate_destination(req.URL)
},
}
// 读取响应时使用带上限的 Reader,避免无限制占用内存
limited := io.LimitReader(resp.Body, 2*1024*1024)
示例中的限制值需要根据业务实际情况调整。关键是让超时、大小和重定向次数成为明确的安全配置,而不是依赖库的默认行为。
八、白名单设计:能收敛范围就不要依赖黑名单
对于企业内部的 Webhook、第三方回调和内容抓取功能,最稳妥的方式通常是建立明确的目标白名单。白名单可以包含域名、协议、端口和路径,并在服务端配置中维护,而不是由用户任意提交目标。
如果业务确实需要访问任意公网地址,可以采用分层策略:
- 协议白名单:只允许 HTTP 和 HTTPS。
- 端口白名单:优先限制为必要端口。
- 地址校验:拒绝回环、私网、链路本地、保留和多播地址。
- 解析一致性:校验每次实际连接对应的 IP。
- 网络隔离:让请求服务运行在受限的独立网络中。
- 出口控制:通过防火墙或代理限制可访问的网段。
- 权限隔离:服务使用低权限账号,不授予读取敏感文件或访问管理接口的能力。
九、日志与检测:让异常请求可被发现
修复并不意味着可以忽略监控。URL 访问服务应记录足够的安全审计信息,但避免直接记录可能包含令牌的完整 URL。推荐记录以下字段:
- 请求用户或调用方的身份标识。
- 目标域名、端口、协议和最终解析地址。
- 是否发生重定向,以及重定向次数。
- 请求耗时、状态码、响应大小和失败原因。
- 命中的安全策略,例如私网地址、非法协议或超出大小限制。
- 任务 ID 和业务操作类型。
监控侧可以重点关注:短时间内大量不同内网域名解析失败、访问地址集中落在保留网段、频繁触发重定向限制、同一用户提交大量异常端口,以及请求延迟与内部服务特征高度相关等情况。
十、修复后的测试清单
完成修复后,建议使用自动化测试覆盖解析、连接和重定向三个阶段,而不是只测试几个常见 URL。
- 正常的 HTTP、HTTPS 公网地址。
- 缺少协议、协议大小写混合和非法协议。
- 带用户名信息、异常端口和格式错误的 URL。
- IPv4、IPv6、回环地址、私有地址、链路本地地址和保留地址。
- 解析到多个地址,其中一个地址不在允许范围内的域名。
- 初始地址合法、后续跳转地址不合法的场景。
- 超大响应、慢响应、连接失败和证书错误。
- 重复跳转、循环跳转和跨协议跳转。
- 不同 HTTP 客户端配置、代理环境变量和容器网络策略。
测试结果除了验证“请求是否被拒绝”,还要确认失败时没有泄露内部 IP、DNS 记录、代理地址、文件路径或底层异常堆栈。
十一、审计中的常见误区
- 只检查字符串前缀:没有经过规范化解析,容易出现主机位判断错误。
- 只拦截几个私网网段:遗漏 IPv6、链路本地、保留地址和特殊解析结果。
- 只校验首次请求:忽略重定向导致的第二跳绕过。
- 认为禁止读取响应就安全:请求本身可能触发内部服务状态变化,也可能泄露时间差异。
- 只修一个接口:同一项目中的图片、文档、Webhook、导入和回调功能可能共享类似代码。
- 只做应用层防护:应用层判断失误时,没有网络出口策略可以兜底。
总结
SSRF 审计的核心不是寻找一条“神奇的 URL”,而是还原服务端从输入、解析、DNS、连接、重定向到响应处理的完整行为。可靠的防护通常需要多层配合:严格解析协议和主机,校验每次连接的实际地址,限制重定向和响应资源,使用目标白名单,并通过网络出口策略建立最后一道边界。
从工程实践看,最值得优先投入的工作有三项:第一,收敛所有服务端代请求逻辑,避免各业务自行实现;第二,把 DNS 校验、重定向校验和响应限制沉淀为统一组件;第三,为请求服务配置独立网络和最小权限。这样即使某个业务校验出现遗漏,风险也不会直接扩展到整个内网。

服务器、云资产与权限边界梳理示意
推荐意见