跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替用户发一次请求”这么简单。真正危险的地方在于,服务端所在网络通常拥有比普通用户更高的访问权限,可能能够连接内部管理端口、云平台元数据服务、仅监听本机的调试接口,甚至访问带有内部信任关系的业务系统。

在实际审计中,SSRF 往往藏在图片抓取、Webhook、文件导入、URL 预览、PDF 生成、在线检测等功能里。单纯依靠关键字搜索容易漏报,也容易把正常的 HTTP 客户端误判为漏洞。比较可靠的方法,是从数据流入手,结合 URL 解析、DNS 解析、重定向处理和网络出口策略进行完整分析。

一、问题场景与风险边界

典型场景是后端接收一个由用户控制的 URL,然后在服务器端发起请求。例如:

String target = request.getParameter("url");
String body = restTemplate.getForObject(target, String.class);
return ResponseEntity.ok(body);

这段代码的问题不在于使用了哪一个 HTTP 客户端,而在于请求目标完全由外部输入决定,且没有建立明确的访问边界。攻击者可能尝试让服务端访问:

  • 本机回环地址或本机监听的管理接口;
  • 企业内网中的非公开服务;
  • 云环境中的实例元数据服务;
  • 开发阶段遗留的调试端点;
  • 能够触发敏感操作的内部 API。

在授权测试和代码审计中,应将验证范围限制在自有测试环境、明确授权的资产和无敏感数据的模拟服务上。不要把真实云环境、生产内网或第三方服务作为验证目标。

二、审计时如何定位 SSRF 数据流

第一步不是搜索“SSRF”三个字,而是寻找“用户输入进入网络请求”的完整路径。常见入口包括以下几类:

  • 请求参数:url、link、callback、webhook、source、redirect 等;
  • JSON 或 XML 字段中的远程地址;
  • 上传文件中的外部资源引用,例如 HTML、SVG、Markdown;
  • 后台任务、消息队列或定时任务中的抓取地址;
  • 第三方 SDK 的 URL 预览、图片下载和文档转换功能。

随后需要继续追踪数据是否经过以下处理:

  • 是否进行了 URL 解析,还是仅用字符串前缀判断;
  • 是否校验了协议、主机名、端口和路径;
  • 是否在校验后重新解析 DNS;
  • 是否允许自动跟随重定向;
  • 是否存在代理、DNS 缓存或多网卡导致的出口差异;
  • 响应内容是否直接返回给用户,还是仅作为后台任务使用。

对于 Java 项目,可以重点搜索 URL、URI、HttpClient、OkHttp、RestTemplate、WebClient、URLConnection 等调用点。Python 项目则应关注 requests、urllib、httpx、aiohttp 等库。搜索只是起点,必须继续确认数据是否可控以及请求是否真正从服务端发出。

三、一个容易被忽略的解析问题

很多修复方案只做了简单的字符串判断,例如判断目标地址是否以“ example.com,因为 URL 语法中还存在用户信息、端口和主机名等部分。

正确做法是先使用标准库解析 URL,再对结构化字段进行校验。下面是一个简化的 Python 示例,重点是说明校验顺序,实际项目仍应结合业务白名单和网络出口策略:

from urllib.parse import urlparse
import ipaddress
import socket

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


def is_public_ip(value: str) -> bool:
    try:
        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
        )
    except ValueError:
        return False


def validate_target(raw_url: str) -> str:
    parsed = urlparse(raw_url)

    if parsed.scheme 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")
    if parsed.hostname.lower().rstrip(".") not in ALLOWED_HOSTS:
        raise ValueError("host is not allowed")

    # 仅用于说明风险检查;生产代码还需要处理 DNS 解析失败、IPv6 等情况
    resolved = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
    for item in resolved:
        ip = item[4][0]
        if not is_public_ip(ip):
            raise ValueError("resolved address is not public")

    return parsed.geturl()

这个示例仍然不能单独解决 DNS 重绑定问题。原因是校验阶段解析出的地址,与实际建立连接时使用的地址可能不是同一个。更稳妥的实现方式是让解析、地址过滤和连接建立使用同一套受控解析逻辑,或者将抓取服务放在独立的网络隔离区,并通过出口代理执行最终访问。

四、为什么“只禁止内网 IP”仍然不够

SSRF 防护经常被简化成“解析域名,发现是内网地址就拒绝”。这比完全不校验要好,但仍然有几个常见缺陷。

1. DNS 解析与连接时机不一致

如果应用先解析域名做检查,随后再把原始域名交给 HTTP 客户端建立连接,客户端可能重新解析该域名。两次解析结果不同,就可能导致校验和实际连接不一致。

2. 重定向绕过初始校验

应用校验了第一个 URL,但 HTTP 客户端自动跟随重定向后,最终请求可能落到未允许的目标。防护策略应明确是否允许重定向。安全性要求较高时,建议默认禁止;确有业务需要时,应对每一次重定向重新执行完整校验,并限制重定向次数。

3. IPv4 与 IPv6 处理不完整

只检查 IPv4 私有网段会遗漏 IPv6 回环地址、链路本地地址和其他特殊地址。地址判断应使用标准 IP 解析库,而不是依赖正则表达式。

4. 代理配置改变了真实出口

HTTP 客户端可能继承环境变量或框架级代理配置。审计时需要确认请求最终经过哪里,不能只看应用代码中的 URL。容器、Sidecar、服务网格和企业代理都可能改变网络路径。

五、推荐的安全实现方式

如果业务允许,最可靠的策略通常是“目的地白名单”,而不是试图穷举所有危险地址。白名单至少应包含协议、主机、端口和必要的路径范围,并尽量避免允许用户直接控制端口。

public URI validateTarget(String raw) {
    URI uri = URI.create(raw);

    if (!"https".equalsIgnoreCase(uri.getScheme())) {
        throw new IllegalArgumentException("only https is allowed");
    }
    if (uri.getUserInfo() != null) {
        throw new IllegalArgumentException("userinfo is not allowed");
    }

    String host = uri.getHost();
    if (host == null || !host.equalsIgnoreCase("cdn.example.com")) {
        throw new IllegalArgumentException("host is not allowed");
    }

    int port = uri.getPort();
    if (port != -1 && port != 443) {
        throw new IllegalArgumentException("port is not allowed");
    }

    return uri;
}

如果业务无法使用固定域名白名单,例如需要抓取客户配置的站点,则建议采用分层防护:

  • 仅允许 http 或 https,拒绝 file、ftp、gopher 等无业务必要的协议;
  • 拒绝用户名、密码、异常端口和不符合规范的主机名;
  • 解析所有 A 和 AAAA 记录,禁止回环、私有、链路本地、保留和组播地址;
  • 禁止或严格限制重定向,并对每次跳转重新检查;
  • 限制响应大小、连接时间、读取时间和并发数;
  • 将抓取服务部署到独立网络区域,通过防火墙或出口代理限制可访问网段;
  • 不把远程响应原样返回给用户,尤其是可能包含凭据或内部错误信息的响应。

六、在授权环境中的安全验证方法

验证 SSRF 不需要访问真实内网资源。可以在本地或隔离测试网络中启动一个无敏感数据的 HTTP 服务,记录请求来源、Host、路径和请求头,然后让待测应用访问该测试服务。

python3 -m http.server 18080 --bind 127.0.0.1

如果需要观察请求细节,可以使用专门的测试服务或在自有容器中编写简单的日志接口。验证重点包括:

  • 请求是否确实由服务端发起,而不是浏览器直接发起;
  • 服务端请求的源地址和网络出口是否符合预期;
  • 应用是否跟随重定向;
  • 响应内容是否会被原样返回;
  • 超时、异常和非 2xx 响应是否会泄露内部信息。

测试报告中应记录入口参数、代码路径、请求时间、测试服务日志和修复建议。不要使用云元数据地址、生产管理端口或第三方站点作为证明材料,这些目标既没有必要,也容易造成越权访问。

七、代码审计中的常见误区

  • 只看黑名单。 黑名单很难覆盖所有地址表示方式和解析差异,优先采用白名单。
  • 只校验 URL 字符串。 应使用结构化解析结果判断协议、主机、端口和用户信息。
  • 校验一次就结束。 DNS 解析、重定向和最终连接都可能改变目标,关键步骤需要重复约束。
  • 忽视响应处理。 即使不能读取响应,SSRF 仍可能造成内部请求;如果响应可回显,风险通常更高。
  • 只在应用层修复。 网络隔离、出口代理和最小权限同样重要,不能把全部安全性寄托在一段输入校验代码上。
  • 没有资源限制。 SSRF 常与文件下载、连接耗尽和大响应导致的内存问题叠加,应设置大小、时间和并发限制。

八、修复后的回归测试清单

修复完成后,建议将以下场景加入自动化测试或安全回归测试:

测试项预期结果
非允许协议拒绝请求,不建立网络连接
未在白名单中的域名拒绝请求
本机、私有和链路本地地址拒绝请求
IPv4 与 IPv6 特殊地址均能正确识别并拒绝
带用户信息或异常端口的 URL拒绝请求
跳转到不允许目标禁止跳转或在跳转前拒绝
超大响应和慢响应触发大小或时间限制
解析失败、连接失败和证书错误返回通用错误,不泄露内部细节

九、总结

SSRF 的核心不是某个特殊 Payload,而是服务端把外部可控数据当成网络目标使用。高质量的审计应围绕完整数据流展开:确认输入来源,定位请求 sink,分析 URL 解析与 DNS 行为,检查重定向和代理配置,再结合网络隔离判断实际影响。

修复时,优先采用明确的目的地白名单;如果业务必须访问任意外部站点,则应同时落实协议限制、地址过滤、重定向控制、超时和响应大小限制,以及独立网络出口。最后通过本地隔离服务完成验证,并将关键场景固化为回归测试,避免同类问题在其他抓取功能中重复出现。

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

0篇意见

推荐意见

没有意见。

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