跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)在实际项目中并不一定表现为一个明显的“请求外部 URL”接口。图片抓取、Webhook 调试、文件导入、URL 预览、第三方回调检测等功能,都可能让服务器代替用户发起网络请求。审计这类问题时,单纯搜索 requests.get 或 HttpClient 往往不够,更重要的是沿着“用户可控输入—URL 解析—网络连接—响应处理”的完整链路确认风险边界。

一、问题背景与典型场景

SSRF 的核心不是“服务器访问了一个 URL”,而是攻击者能够影响服务器访问的目标、协议、端口或请求内容,并借助服务器所在网络环境获得普通客户端无法获得的访问能力。

  • 图片或网页预览:后端接收 URL,下载内容后返回给前端。
  • Webhook 测试:用户输入回调地址,服务端向该地址发送测试请求。
  • 文档解析:服务端根据文档中的外部资源地址加载图片、字体或样式。
  • 云资源集成:后端根据用户提交的地址访问对象存储、内部 API 或代理服务。
  • 健康检查功能:管理员输入地址,系统主动探测服务是否可用。

这些功能本身未必存在问题,真正需要关注的是:目标地址是否完全可控,校验发生在 DNS 解析前还是后,是否允许跟随重定向,以及请求是否携带了内部身份凭据。

二、审计时先画出完整数据流

建议不要从某一个危险函数开始,而是先建立数据流图。一个典型链路如下:

HTTP 参数 / JSON 字段
        ↓
URL 规范化与解析
        ↓
协议、主机、端口校验
        ↓
DNS 解析
        ↓
建立 TCP / TLS 连接
        ↓
发送请求并处理重定向
        ↓
读取响应、保存文件或转发给用户

审计时可以分别回答以下问题:

  • 输入来自哪里,是否经过了鉴权和权限校验?
  • 校验的是原始字符串、解析后的主机名,还是最终连接的 IP 地址?
  • 是否允许 http 以外的协议,例如 file、gopher 或自定义协议?
  • DNS 解析是否可能返回多个地址,程序是否逐个检查?
  • 请求是否自动跟随 30x 重定向,重定向后的地址是否重新校验?
  • 是否存在代理配置、IPv6、Unix Socket 或其他非预期的网络路径?
  • 响应内容是否会被当作 HTML、XML、图片或压缩包继续解析?

三、最容易被忽略的校验绕过点

1. 只做字符串黑名单

例如仅判断 URL 中是否包含 127.0.0.1、localhost 或某个内部域名。这种做法很脆弱,因为同一个地址可能存在不同表示形式,IPv4、IPv6、十进制整数、短格式地址以及大小写和尾点等写法,都可能导致字符串判断与实际连接目标不一致。

正确思路是先使用标准 URL 解析器拆分协议、主机、端口和路径,再对规范化后的主机进行解析,并对解析结果执行 IP 分类判断。校验对象应该是“最终要建立连接的 IP”,而不是未经解析的原始字符串。

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

有些实现先调用 DNS 解析判断地址是否安全,之后又把原始域名交给 HTTP 客户端建立连接。两次解析的结果可能不同,尤其是在攻击者可控 DNS、短 TTL 或代理环境下,检查结果和实际连接目标可能不一致。

更稳妥的做法是:

  • 由应用统一完成解析并取得地址列表。
  • 对所有解析结果进行安全判断,而不是只检查第一个地址。
  • 建立连接时固定使用已校验的地址,同时保留正确的 Host 和 TLS Server Name。
  • 在网络层再增加出口控制,避免应用层校验失效后直接访问敏感网段。

3. 重定向导致校验失效

初始地址可能是公开站点,但服务器返回重定向后,目标变成了内网地址。如果 HTTP 客户端默认跟随重定向,初始校验就失去了意义。

推荐默认关闭自动重定向。如果业务必须支持重定向,应当在每一次跳转前重新执行协议、主机、端口和解析结果校验,并限制最大跳转次数。

四、一个防御性 URL 校验示例

下面示例展示的是审计和防护思路,不负责发起请求。实际项目中还需要结合网络出口策略和 HTTP 客户端的连接控制。

from ipaddress import ip_address
from urllib.parse import urlparse
import socket

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


def resolve_all(hostname, port):
    result = socket.getaddrinfo(
        hostname,
        port,
        type=socket.SOCK_STREAM,
    )
    return {item[4][0] for item in result}


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


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

    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError("unsupported scheme")
    if not parsed.hostname:
        raise ValueError("missing hostname")

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

    addresses = resolve_all(parsed.hostname, port)
    if not addresses:
        raise ValueError("hostname has no address")

    if not all(is_public_ip(addr) for addr in addresses):
        raise ValueError("destination is not public")

    return {
        "scheme": parsed.scheme,
        "hostname": parsed.hostname,
        "port": port,
        "path": parsed.path or "/",
    }

这个示例有几个值得注意的点:只允许业务需要的协议;端口采用白名单而不是简单排除少数端口;对 DNS 返回的全部地址进行检查;同时拒绝回环、私有、链路本地、组播、保留和未指定地址。

不过,应用层代码仍然可能受到解析与连接之间的竞态影响。因此生产环境不应只依赖这段逻辑,还应通过防火墙、容器网络策略、云安全组或 egress proxy 限制服务端出站访问。

五、HTTP 客户端配置中的关键防线

审计代码时,除了查找 URL 参数,还要检查 HTTP 客户端的默认行为。以下配置项经常决定漏洞是否可以被稳定利用:

配置项风险建议
自动跟随重定向初始地址通过校验后跳转到受限网络默认关闭;每次跳转重新校验
连接超时内网不可达地址造成线程、协程或连接池耗尽设置较短的连接和读取超时
响应体上限下载超大文件导致内存或磁盘资源耗尽限制响应大小并流式读取
代理继承环境变量或全局代理改变实际网络路径明确配置代理,不要无意继承
协议处理器非 HTTP 协议带来本地文件或其他资源访问能力显式限制为业务所需协议
请求头转发把用户请求中的 Cookie、Authorization 等凭据转发给目标建立明确的请求头白名单

六、代码审计中的搜索与回溯方法

在大型代码库中,可以先进行关键词粗筛,再结合调用链回溯。不同语言的关键词不完全相同,但思路一致。

  • 输入来源:url、uri、callback、webhook、image、preview、proxy。
  • 网络请求:requests、urllib、HttpClient、RestTemplate、WebClient、axios。
  • 底层连接:socket、connect、getaddrinfo、net.Dial。
  • 重定向和代理:followRedirects、redirect、proxy、NO_PROXY。

找到请求函数后,不要立即下结论。需要继续向上确认输入是否可控,向下确认是否存在 DNS 校验、重定向限制、超时和响应体限制。对封装较深的项目,还应关注统一 HTTP SDK、网关代理和异步任务消费者,因为真正的请求可能不在控制器或路由函数中。

七、在隔离环境中进行安全验证

漏洞验证应尽量使用自建的测试服务,不要扫描或请求真实内网、云平台管理接口及第三方系统。可以在本地或专用测试网络中启动两个服务:一个作为公开入口,另一个模拟受限服务,并在日志中记录访问来源和请求内容。

# 启动一个仅用于测试的本地 HTTP 服务
python3 -m http.server 18080 --bind 127.0.0.1

# 调用待测应用的 URL 预览接口,具体路径按测试项目调整
curl --max-time 5 \
  -H 'Content-Type: application/json' \
  -d '{"url":" \
  

验证重点不是获取敏感信息,而是确认应用是否真的代替客户端发起了请求。可以通过测试服务日志、应用审计日志、出站防火墙日志和请求链路追踪共同确认。测试结束后应清理临时服务、测试数据和放行规则。

八、修复方案不能只停留在黑名单

较完整的防护通常分为四层:

  • 业务层:尽量不要让用户直接提交任意 URL。对于图片、Webhook 等功能,使用域名或资源 ID 白名单。
  • 应用层:限制协议、端口、跳转次数、响应大小和超时时间;对解析后的全部 IP 做分类检查。
  • 网络层:服务默认禁止访问管理网段、容器网段、云平台链路本地地址和不必要的内部服务,仅放行明确的外部目的地。
  • 监控层:记录目标域名、解析地址、端口、响应状态、耗时、重定向链和拒绝原因,建立异常访问告警。

如果业务确实需要访问外部资源,可以考虑引入专用出站代理。应用只向代理提交经过规范化的目标信息,由代理统一执行域名白名单、DNS 校验、IP 限制、内容大小限制和审计记录。这样比让每个业务模块自行实现一套校验逻辑更容易维护。

九、容易遗漏的延伸风险

  • 响应内容二次解析:下载的 XML、SVG、HTML 或压缩包可能继续触发外部资源加载或路径穿越问题。
  • 凭据泄露:统一 HTTP 客户端可能自动附加服务账号令牌、内部 Cookie 或代理认证信息。
  • 资源耗尽:目标服务长时间不响应、持续返回大文件或返回大量重定向,都可能形成拒绝服务。
  • IPv6 双栈差异:只检查 IPv4 而忽略 IPv6,会造成策略不一致。
  • 域名白名单过宽:允许整个组织域名后,子域接管、测试环境和遗留服务可能扩大访问范围。
  • 异步任务绕过鉴权:前端接口做了限制,但后台任务队列消费时没有重新校验目标地址。

十、审计结论应如何记录

一份可复用的漏洞记录至少应包含以下内容:

  • 受影响功能、接口和参数。
  • 从输入到网络请求的完整调用链。
  • 是否允许任意协议、端口、重定向和响应类型。
  • 解析前校验与最终连接目标是否一致。
  • 在隔离环境中的验证结果和日志证据。
  • 业务影响、受影响网络范围以及是否可能泄露服务端凭据。
  • 短期修复、长期架构改进和回归测试用例。

回归测试不应只测试一个被禁止的地址,还应覆盖重定向、多个 DNS 记录、IPv6、异常端口、超大响应、超时、非法协议和异步任务等场景。否则修复很容易只针对当前样例,而没有真正收紧请求能力。

总结

SSRF 审计的关键不在于记住若干黑名单地址,而在于明确“用户控制的 URL 最终连接到了哪里”。可靠的判断应覆盖 URL 解析、DNS 解析、实际连接、重定向和响应处理全过程,并由应用策略与网络出口策略共同兜底。

从工程角度看,最值得优先落实的措施是:限制业务需求范围、关闭或重新校验重定向、对所有解析地址进行检查、设置超时和响应上限、禁止转发敏感请求头,并在网络层实施默认拒绝的出站策略。这样即使某个业务模块的校验出现遗漏,也不会轻易演变成对内部服务和凭据的横向影响。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.