跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不是简单的“让服务器替客户端发一个 HTTP 请求”。在实际项目中,它往往与 URL 解析差异、DNS 解析、重定向、代理配置以及云环境网络边界同时出现,导致漏洞影响被低估。本文以代码审计和授权测试为主线,整理一套适用于业务系统的 SSRF 分析方法,重点放在漏洞成因、低风险验证和工程化修复。

一、典型业务场景与风险边界

常见的 SSRF 入口包括:远程图片抓取、网页缩略图生成、PDF 或文档转换、Webhook 调试、第三方 URL 预览、RSS 导入、代码仓库地址导入,以及根据用户输入探测服务健康状态等功能。

这类功能通常具有一个共同特征:服务端接收用户提供的 URL,随后由后端发起网络请求并处理响应。如果请求目标和协议没有被严格限制,攻击者就可能诱导服务器访问不应暴露的网络资源。

  • 访问应用所在网络中的管理接口、测试服务或内部 API。
  • 借助服务器身份访问仅对内网开放的资源。
  • 通过响应差异推断端口或服务是否存在。
  • 在某些环境中读取不应由业务服务访问的配置接口。

需要特别强调的是,SSRF 的影响取决于网络拓扑、服务权限和出站访问策略。不能仅凭存在 URL 请求功能,就直接判定为高危;应结合实际可达范围和数据暴露情况进行分级。

二、代码审计时如何定位 SSRF 入口

审计时不要只搜索 curl 或 requests。更有效的方法是从“用户可控数据如何流向网络请求”入手,建立输入到危险函数的完整数据流。

1. 先找输入源

  • HTTP 参数、JSON 字段、表单字段和上传文件中的 URL。
  • 数据库中由用户创建或修改的回调地址。
  • 消息队列、Webhook、任务调度和导入文件中的远程地址。
  • 环境变量或配置中心中可被低权限用户间接修改的地址。

2. 再找网络请求点

  • Java 中的 HttpClient、OkHttp、URLConnection。
  • Python 中的 requests、urllib、httpx。
  • Go 中的 http.Get、http.Client.Do。
  • Node.js 中的 fetch、axios、http.request。
  • 图片、PDF、Office 转换组件,以及底层会自动解析外部资源的库。

3. 检查中间处理逻辑

很多问题不在请求函数本身,而在请求前后的处理。例如只校验了 URL 的字符串前缀、先校验域名后重新解析、允许自动跟随重定向,或者对异常响应处理不当。审计报告中应记录完整调用链,而不是只标记某一行代码。

三、漏洞成立的关键判断

一个可被利用的 SSRF 通常同时满足以下条件:

  1. 攻击者能够控制请求目标,或控制目标中的关键部分。
  2. 服务端确实会发起网络请求,而不是仅在前端展示链接。
  3. 服务端网络位置与攻击者不同,存在额外的网络可达性。
  4. 请求结果、状态码、响应时间或错误信息能够被攻击者观察到。
  5. 目标解析和访问过程中没有可靠的协议、地址和重定向限制。

如果服务端只允许访问固定的可信域名,且在连接前后都进行了地址校验,同时禁用了不必要的重定向,那么即使存在 URL 输入,也不一定构成有效 SSRF。

四、低风险验证方法

在授权测试中,优先使用自建的回显服务或测试域名验证请求是否从服务端发出,不要直接探测生产环境内部系统。验证目标应当是确认请求来源和控制链路,而不是读取敏感数据。

可以在隔离环境中启动一个只记录请求元数据的测试服务:

python3 -m http.server 18080 --bind 127.0.0.1

更适合团队协作的方式,是使用内部测试域名,并在 DNS 或网关侧记录请求时间、来源地址、Host 和 User-Agent。测试响应只返回固定文本,不提供目录列表、环境变量或其他敏感信息。

验证时建议按以下顺序进行:

  • 使用明确允许的公开测试地址,确认服务端确实发起请求。
  • 检查请求是否经过代理,记录实际出口地址和协议。
  • 在不访问敏感资源的前提下,测试应用是否允许重定向。
  • 测试非法协议、缺少主机名、异常端口等输入能否被稳定拒绝。
  • 确认错误信息不会回显内网地址、解析结果、认证头或完整响应内容。
安全验证的目标是证明风险边界,而不是最大化读取内容。对于内部地址、云平台管理接口和生产凭据接口,应使用模拟服务或人工审批后的专用测试端点。

五、最容易被忽略的解析与校验问题

1. 字符串前缀校验不可靠

类似“URL 必须以某个域名开头”的判断容易受到端口、用户信息、大小写、编码和解析差异影响。正确做法是使用标准 URL 解析器分别读取协议、主机、端口和路径,再对结构化字段进行校验。

2. DNS 解析与连接之间存在时间窗口

如果程序先解析域名判断其是否为公网地址,之后又让底层 HTTP 客户端重新解析一次,域名解析结果可能发生变化。防护逻辑应尽量让校验结果与最终连接目标绑定,必要时使用受控 DNS 解析器,并在建立连接时再次确认地址。

3. 自动重定向会绕过首轮校验

首个 URL 可能是允许访问的域名,但响应返回重定向后,客户端又访问了受限地址。应默认关闭自动重定向,或者对每一次跳转重新执行协议、主机、端口和 IP 地址校验,并限制最大跳转次数。

4. IPv4 与 IPv6 规则不一致

只判断常见 IPv4 私网段是不够的。校验逻辑还应覆盖回环地址、链路本地地址、未指定地址、IPv4 映射 IPv6 地址以及组织内部使用的保留网段。建议使用成熟网络地址库,而不是手工编写字符串判断。

5. 代理配置改变了真实出站路径

很多 HTTP 库会自动读取 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 等环境变量。审计时应确认生产环境是否启用了代理,以及代理是否会重新解析目标地址。对于不需要代理的服务,应显式关闭环境代理继承,避免开发环境和生产环境行为不一致。

六、一个较稳妥的防护实现思路

下面的示例只展示防护流程,不包含对真实内部资源的访问。实际项目中还需要结合统一网络出口、审计日志和业务白名单进行设计。

from urllib.parse import urlparse
import ipaddress
import socket

ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}


def is_public_ip(value):
    ip = ipaddress.ip_address(value)
    return not (
        ip.is_private or
        ip.is_loopback or
        ip.is_link_local or
        ip.is_reserved or
        ip.is_multicast or
        ip.is_unspecified
    )


def validate_target(raw_url):
    parsed = urlparse(raw_url)

    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        raise ValueError("unsupported scheme")
    if not parsed.hostname:
        raise ValueError("missing hostname")
    if parsed.username or parsed.password:
        raise ValueError("userinfo is not allowed")

    port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
    if port not in ALLOWED_PORTS:
        raise ValueError("unsupported port")

    addresses = socket.getaddrinfo(
        parsed.hostname, port, type=socket.SOCK_STREAM
    )
    resolved = {item[4][0] for item in addresses}
    if not resolved or not all(is_public_ip(item) for item in resolved):
        raise ValueError("target address is not allowed")

    return parsed, resolved

这个示例仍然不能直接作为通用生产组件使用,原因包括 DNS 重新解析、连接复用、代理、重定向和域名白名单策略等。更推荐将访问能力集中封装成一个内部服务,统一实现超时、大小限制、响应类型限制和审计,而不是让业务代码到处直接调用 HTTP 客户端。

七、工程化防护清单

控制项建议
协议只允许业务真正需要的 HTTP 或 HTTPS,拒绝文件、脚本、字典和其他非业务协议。
主机优先使用严格域名白名单;无法白名单时,至少拒绝本地、私有、保留和链路本地地址。
端口限制到明确业务端口,禁止任意端口访问。
DNS统一解析策略,避免校验解析和连接解析不一致,并关注 DNS 变更。
重定向默认关闭;确需开启时逐跳校验并限制跳转次数。
请求头不要把用户的 Cookie、Authorization 或内部追踪头转发到目标站点。
响应处理限制响应体大小、连接时间和总耗时,按需限制 Content-Type,避免直接回显完整响应。
网络层在出口防火墙、云安全组和代理层再次限制出站范围,形成纵深防御。
日志记录业务用户、目标域名、解析地址、状态码、耗时和拒绝原因,但避免记录敏感响应内容。

八、代码审计报告应该写清什么

一份可复现、可修复的 SSRF 报告至少应包含以下内容:

  • 漏洞入口及其权限要求。
  • 用户输入到网络请求函数的完整调用链。
  • 当前支持的协议、端口、重定向和代理行为。
  • 在隔离环境中观察到的服务端请求证据。
  • 实际影响范围,例如只能访问公网,还是可以触达受限网络。
  • 不涉及敏感数据的复现步骤和测试地址。
  • 短期修复措施与长期架构改进建议。

如果无法确认内部网络可达性,应如实标记为“潜在 SSRF”或“服务端请求行为待确认”,不要在报告中夸大影响。反过来,如果已经确认请求会经过高权限代理或能够访问内部管理面,也应将网络拓扑和权限边界写清楚。

九、修复后的回归测试

修复不能只验证一个被拒绝的 URL。建议建立覆盖解析差异的自动化测试集,至少包括:

  • 合法的 HTTP 和 HTTPS 地址。
  • 缺少协议、缺少主机名和非法端口。
  • 用户名密码形式的 URL。
  • IPv4、IPv6、回环、私网、链路本地和保留地址。
  • 解析到多个地址且其中包含受限地址的域名。
  • 返回重定向到受限目标的测试服务。
  • 超时、超大响应、错误 Content-Type 和连接异常。
  • 代理环境变量存在与不存在时的行为差异。

同时应在网络层保留一份独立的拒绝策略。应用层校验出现遗漏时,出口防火墙仍然能够阻止服务访问不应到达的网段。

总结

SSRF 审计的核心不是寻找某个特殊字符串,而是理解“输入解析、DNS 解析、建立连接、重定向和响应处理”这一整条链路。可靠的防护通常由四部分组成:严格限制协议和目标、避免解析与连接之间的差异、关闭或重新校验重定向,以及在网络出口实施独立的访问控制。

在实际项目中,优先采用域名白名单、受控出站代理和统一请求组件,配合低风险的回显服务完成验证。这样既能提高漏洞判断的准确性,也能避免测试过程对生产系统和内部敏感资源造成额外影响。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

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