跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)在代码审计中很常见。它往往不是一个单独的“危险函数”问题,而是由 URL 解析、DNS 解析、重定向、代理配置和访问控制共同决定的。实际排查时,最容易出现两种情况:一是把所有服务端请求都当成漏洞,产生大量误报;二是只拦截几个常见内网地址,修复后仍然可以通过 IPv6、DNS 解析差异或重定向绕过。

本文以一个常见的“远程资源预览”功能为例,讨论如何在授权测试环境中确认 SSRF 风险、定位触发条件,并设计更可靠的修复方案。示例只使用本机或隔离实验网络,不涉及访问第三方系统、云平台元数据或未授权资源。

一、问题场景与风险边界

假设应用提供如下接口:用户提交一个 URL,服务端抓取内容后生成预览。伪代码如下:

public String preview(String url) {
    return httpClient.get(url).getBody();
}

如果这个 URL 完全由用户控制,服务端通常具备比普通浏览器更高的网络可达性。攻击者可能借此让应用访问:

  • 应用所在主机的本地管理接口;
  • 容器、虚拟机或内网中的其他服务;
  • 仅允许服务器访问的内部 API;
  • 由于错误配置而暴露的调试、监控或配置接口。

但需要注意,服务端发起请求本身并不等于存在 SSRF。审计结论至少应回答三个问题:请求目标是否可控、请求是否由服务器发出、服务器网络权限是否使该行为产生越权影响。若应用只允许访问固定域名列表,并且网络层也进行了隔离,就不能仅凭存在 HTTP 客户端调用认定为高危漏洞。

二、代码审计时的追踪方法

审计可以从“输入源—URL 处理—请求发送—响应回显”四个环节入手。

1. 查找输入源

重点关注请求参数、JSON 字段、数据库中可被用户修改的字段,以及消息队列中的 URL。不要只搜索 url 变量名,实际项目中常见的命名还有 link、callback、image、webhook、redirect 和 source。

2. 跟踪 URL 是否经过可信约束

很多项目存在类似下面的“看起来像校验”的代码:

if (url.startsWith(" {
    return client.get(url);
}

这种前缀判断并不能证明目标是 example.com。URL 语法允许用户名、端口和路径等组件,字符串前缀与实际主机名不是一回事。正确做法是先使用标准 URL 解析器提取主机,再对规范化后的主机和解析地址执行策略判断。

3. 关注重定向和二次请求

即使初始 URL 是允许的,服务端也可能跟随 30x 跳转访问另一个地址。审计时需要确认 HTTP 客户端是否默认跟随重定向,以及重定向后的目标是否重新执行协议、主机和 IP 校验。许多实际漏洞并不在第一跳,而是在第二跳。

4. 确认响应是否可见

SSRF 不一定要求响应内容直接返回给用户。只要服务端能够访问一个本不应访问的目标,便可能造成端口探测、状态判断或副作用请求。响应回显会增加数据泄露风险,但不是判断 SSRF 的必要条件。

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

验证时建议建立两个本地服务:一个模拟业务服务,另一个模拟不应被访问的内部服务。不要使用真实生产地址、云元数据地址或第三方站点。

可以用 Python 启动一个仅监听本机回环地址的测试服务:

mkdir -p /tmp/ssrf-lab
printf 'internal-test-response\n' > /tmp/ssrf-lab/index.html
python3 -m http.server 18080 --bind 127.0.0.1 --directory /tmp/ssrf-lab

随后在应用测试环境中提交一个指向本地测试服务的 URL,并观察应用服务器的访问日志。验证重点不是“能否打到某个系统”,而是确认调用链:

  • 请求是否确实从应用服务器发出;
  • 应用是否允许访问回环地址、私有地址或链路本地地址;
  • 是否自动跟随重定向;
  • 是否限制了协议、端口、响应大小和超时时间;
  • 错误信息是否泄露了 DNS、连接或内部网络细节。

在测试工具层面,可以使用本地日志或抓包确认请求来源。例如,只对本机服务执行:

curl -i 

不要把生产应用的 URL 直接交给公共回连平台,也不要为了证明漏洞而访问云平台凭据接口。对于真实项目,优先使用项目组提供的隔离测试网段和内部审计标记。

四、常见绕过点与根因分析

1. 只过滤少量字符串

黑名单方式经常只覆盖 127.0.0.1、localhost 等字面量,却忽略了地址的多种表示形式、IPv6 回环地址以及解析后落入私有网段的域名。更稳妥的思路是采用“默认拒绝、明确允许”的白名单策略,并对解析结果进行网络范围判断。

2. DNS 解析与请求连接不一致

如果程序先解析域名做安全判断,随后又把原始域名交给 HTTP 客户端连接,中间可能发生 DNS 变化,形成校验时和连接时的结果不一致。这个问题通常称为 DNS rebinding 或 TOCTOU(检查与使用之间的时间差)。

防护时不能只在业务层调用一次 gethostbyname 就认为安全。应尽量使用能够固定解析结果、对最终连接地址执行策略校验的网络组件,并配合出口防火墙限制应用进程的网络范围。

3. 忽略 IPv6

很多旧代码只判断 IPv4 私网段,却没有处理 IPv6。地址校验应使用标准库的 IP 类型,而不是用字符串正则。对于不需要 IPv6 的服务,可以在网络层明确禁用或限制;需要支持 IPv6 时,则应同时覆盖回环、链路本地、唯一本地地址等范围。

4. 重定向未重新校验

请求白名单域名后,如果客户端自动跟随跳转,最终目标可能已经不在允许范围内。修复时可以关闭自动重定向,由业务代码读取 Location 后重新解析和校验;如果确实需要跳转,也应限制最大跳转次数,并对每一跳执行同样的策略。

5. 代理配置改变了实际出口

部分 HTTP 客户端会读取系统环境变量或默认代理配置。代码审计时不能只看目标 URL,还要确认 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 等配置是否会影响请求路径。生产服务通常应显式配置代理策略,避免测试环境和生产环境行为不一致。

五、一个更可靠的校验示例

下面的 Python 示例用于说明校验思路,不应直接替代经过评审的生产网络组件。它做了协议限制、主机规范化、DNS 解析和 IP 范围判断,并关闭了自动重定向:

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

ALLOWED_SCHEMES = {"https"}
ALLOWED_HOSTS = {"static.example.com"}


def resolve_addresses(hostname, port):
    results = set()
    for item in socket.getaddrinfo(hostname, port, type=socket.SOCK_STREAM):
        results.add(item[4][0])
    return results


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


def validate_target(raw_url):
    parsed = urlparse(raw_url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError("unsupported scheme")
    if not parsed.hostname or parsed.username or parsed.password:
        raise ValueError("invalid host form")

    hostname = parsed.hostname.rstrip(".").lower()
    if hostname not in ALLOWED_HOSTS:
        raise ValueError("host is not allowed")

    port = parsed.port or 443
    addresses = resolve_addresses(hostname, port)
    if not addresses or not all(is_public_address(x) for x in addresses):
        raise ValueError("resolved address is not allowed")
    return parsed.geturl()


def fetch(raw_url):
    target = validate_target(raw_url)
    response = requests.get(
        target,
        allow_redirects=False,
        timeout=(3, 5),
        stream=True,
    )
    response.raise_for_status()
    return response.text[:1024 * 1024]

这个示例仍有工程边界:域名解析结果与底层连接之间的绑定方式取决于 HTTP 客户端实现;连接池、代理、IPv4/IPv6 优先级和容器 DNS 都可能改变实际行为。因此,真正的防护不能只依赖应用代码,还应在网络层建立出站访问控制,例如仅允许访问必要的 DNS、代理和目标网段。

六、修复方案的优先级

措施作用注意事项
业务白名单限制可访问的协议、域名和端口按解析后的主机判断,不使用字符串前缀
关闭或重新校验重定向避免第二跳绕过校验限制跳转次数和响应大小
出口网络隔离即使应用校验失效,也减少可达范围按服务职责配置最小网络权限
请求资源限制降低超时、连接耗尽和大响应风险同时限制连接超时、读取超时和总字节数
日志与告警便于发现异常目标和误用避免记录敏感响应内容和凭据

七、修复验证与回归测试

修复后不要只测试一个被拦截的 URL。建议建立回归用例,覆盖以下类别:

  • 允许域名的正常 HTTPS 请求;
  • 不允许的协议,例如 file、ftp 或其他非业务协议;
  • 回环地址、私有地址、链路本地地址和 IPv6 地址;
  • 大小写、末尾点号、显式端口和 URL 编码等规范化场景;
  • 返回 30x 的测试域名,以及跳转到不允许域名的情况;
  • DNS 解析失败、连接超时、响应过大和证书校验失败。

每个用例都应记录最终解析结果、是否真正建立连接、失败原因和审计日志内容。这样可以区分“输入校验拒绝”“网络策略拒绝”和“目标服务不可用”,避免把网络故障误判为安全修复成功。

八、总结

SSRF 的核心不是简单地屏蔽几个内网 IP,而是控制“应用可以代表谁、通过什么协议、访问哪些目标”。分析时应从输入源追踪到实际连接,重点检查 URL 解析差异、DNS 与连接的一致性、重定向、代理和 IPv6;验证时应坚持本地化、隔离化和最小影响原则。

比较可靠的防线通常是多层组合:业务侧使用明确白名单,HTTP 客户端关闭不必要的重定向和协议,网络侧限制出站范围,日志侧记录目标与决策结果。任何单一校验都可能因库行为或部署环境变化而失效,只有代码、组件和网络策略同时收紧,修复才具有长期稳定性。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.