跳转到帖子

服务端请求伪造(SSRF)并不是“让服务器代替用户访问一个 URL”这么简单。真正危险的地方在于:应用服务器通常处于比普通用户更可信的网络位置,能够访问本地管理接口、云平台元数据服务、内网 API 或仅绑定在回环地址上的调试端口。一旦 URL 参数、Webhook、图片抓取、文档转换等功能缺少边界控制,原本的业务能力就可能变成跨网络边界的请求代理。

本文从代码审计和防守视角,整理一套适用于 Java、Python 等常见后端的 SSRF 分析方法。示例均以本地测试环境和无敏感数据的服务为前提,重点放在漏洞判断、根因定位和修复验证。

一、哪些功能容易引入 SSRF

审计时不要只搜索名为 url、uri、link 的参数,更应关注“服务端根据外部输入发起网络请求”的行为。常见入口包括:

  • 远程图片下载、头像抓取、网页预览。
  • Webhook、回调地址、第三方通知地址。
  • 文件导入、在线 XML 或 Office 文档解析。
  • URL 转 PDF、截图、网页内容转 Markdown。
  • 代理接口、健康检查、连通性测试功能。
  • 根据用户输入访问 Git、对象存储或内部配置中心。

代码层面可以优先检索 HTTP 客户端调用、URL 构造和重定向配置,例如 Java 中的 HttpClient、OkHttp、URLConnection,Python 中的 requests、urllib、httpx,Node.js 中的 axios、fetch、got 等。搜索结果不能直接等同于漏洞,关键是确认请求目标是否受外部输入影响,以及请求进程所在网络区域能访问什么资源。

二、先判断是“可控请求”还是“真正的 SSRF”

审计时建议把判断拆成三个问题:

  • 输入是否可控:攻击者能否直接提交 URL,或通过 JSON、表单、HTTP 头、文件内容间接影响目标地址。
  • 请求是否由服务端发起:浏览器端跳转、前端 fetch 与服务端 HTTP 客户端的风险边界不同。
  • 服务端是否能访问高价值网络:包括本机回环地址、容器网段、云元数据地址、办公网或管理网段。

如果服务端只允许访问固定的第三方域名,且 DNS、重定向和端口均受到约束,风险通常可控。如果服务端接受任意绝对 URL,即使响应内容不会返回给用户,也可能形成盲 SSRF:请求本身可以触发内部服务日志、状态变更、出网探测或带外回连。

一个实用的审计原则是:不要只问“能不能读到响应”,还要问“这个请求是否能产生副作用”。盲 SSRF 同样需要按高风险路径处理。

三、典型漏洞根因:只校验字符串,没有校验最终连接目标

下面是常见但不安全的逻辑示意:

String target = request.getParameter("url");
HttpGet httpGet = new HttpGet(target);
try (CloseableHttpResponse response = client.execute(httpGet)) {
    return EntityUtils.toString(response.getEntity());
}

这段代码的问题不只是“没有白名单”。即使增加了简单的字符串判断,也可能遗漏 URL 解析、DNS 解析、重定向和 IP 表示方式带来的边界问题。

四、审计时需要重点检查的绕过面

1. URL 解析差异

不同语言和库对用户名密码、端口、编码字符、反斜杠以及异常 URL 的解析规则并不完全一致。业务层如果使用一个解析器判断域名,HTTP 客户端又使用另一个解析器建立连接,就可能出现“校验结果”和“实际连接目标”不一致。

建议统一使用成熟 URL 解析库,不要通过 startsWith、contains 或字符串拼接判断域名。例如,以下逻辑并不能证明目标属于 example.com:

if (url.contains("example.com")) {
    // 错误示例:无法证明最终主机就是允许域名
}

2. DNS 解析与 DNS Rebinding

域名校验后再进行连接,存在时间窗口问题。域名第一次解析到公网地址,连接时可能解析到内网地址;也可能解析结果中同时包含公网和私网地址,而客户端最终选择了未被检查的地址。

防守时应在解析阶段取得全部地址,逐个判断是否属于禁止范围,并尽量让后续连接复用已经校验过的解析结果,而不是把原始域名交给客户端再次解析。对于高风险场景,可使用受控 DNS 解析器和固定出口代理,从网络层进一步限制访问范围。

3. IPv4、IPv6 和特殊地址表示

只判断常见 IPv4 私网段是不够的,还应覆盖:

  • 回环地址、链路本地地址、未指定地址。
  • RFC1918 私网、运营商级 NAT 地址和文档规定的保留网段。
  • IPv6 回环、链路本地、唯一本地地址。
  • IPv4-mapped IPv6 地址。
  • 十进制、十六进制或其他库接受的非标准 IPv4 表示。

实际实现中,应优先使用标准 IP 地址解析库,将地址规范化后再判断,而不是自行编写正则表达式。

4. HTTP 重定向

即使初始 URL 通过检查,目标服务器也可能返回 3xx,将客户端引导到新的主机。若 HTTP 客户端默认跟随重定向,最终连接目标可能完全绕过初始校验。

稳妥做法是默认关闭自动重定向;如果业务确实需要跟随,则每一次重定向都重新执行协议、端口、域名和 IP 检查,并限制最大跳转次数。

5. 协议和端口限制

业务只需要下载 HTTPS 图片时,就不要接受 file、ftp、gopher 或其他协议。端口也应采用明确白名单,例如只允许 443,必要时再允许 80。禁止将用户输入直接交给支持多协议的通用客户端。

五、一个可落地的安全请求流程

对于“服务端抓取远程资源”这类功能,可以按以下顺序处理:

  1. 限制 URL 长度,拒绝控制字符和不必要的编码形式。
  2. 只允许明确的协议,例如 HTTPS。
  3. 解析主机名、端口和路径,不接受缺省逻辑不明确的输入。
  4. 如果业务采用域名白名单,使用规范化后的主机名进行精确匹配,而不是后缀模糊匹配。
  5. 解析全部 IP,并拒绝回环、链路本地、私网、保留和未指定地址。
  6. 关闭自动重定向,或对每一跳重新执行完整校验。
  7. 设置连接超时、读取超时、响应体大小和总下载时长。
  8. 下载完成后按业务需要校验内容类型,不要仅信任 Content-Type。
  9. 将请求放在独立网络区域,使用出口防火墙限制可访问的目的网段。

六、Java 实现中的关键控制点

下面示例展示控制思路,生产环境应结合项目使用的 HTTP 客户端和网络架构完善异常处理、日志脱敏及连接池配置:

URI uri = URI.create(userInput);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
    throw new SecurityException("unsupported scheme");
}

String host = uri.getHost();
if (host == null || host.length() > 253) {
    throw new SecurityException("invalid host");
}

int port = uri.getPort() == -1 ? 443 : uri.getPort();
if (port != 443) {
    throw new SecurityException("unsupported port");
}

InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
    if (isNonPublicAddress(address)) {
        throw new SecurityException("non-public address");
    }
}

// 使用已通过校验的连接策略发起请求;关闭自动重定向
// 同时设置连接超时、读取超时和响应体大小上限

其中 isNonPublicAddress 不应只调用一个简单的 isSiteLocalAddress 判断。应根据项目使用的网络库,补充 IPv4 和 IPv6 的回环、链路本地、唯一本地、未指定及保留地址判断。还要注意 DNS 解析结果和实际连接之间的竞态问题,严格场景下应采用固定解析结果的连接实现,或者让受控代理负责解析与出口访问。

七、Python 服务中的实现建议

Python 项目常见问题是直接把用户输入传给 requests.get。至少要明确关闭重定向、设置超时,并在请求前完成地址检查:

from urllib.parse import urlparse
import ipaddress
import socket


def validate_target(value):
    parsed = urlparse(value)
    if parsed.scheme != "https":
        raise ValueError("scheme is not allowed")
    if not parsed.hostname or parsed.port not in (None, 443):
        raise ValueError("host or port is not allowed")

    for item in socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM):
        address = ipaddress.ip_address(item[4][0])
        if (address.is_private or address.is_loopback or
                address.is_link_local or address.is_reserved or
                address.is_unspecified):
            raise ValueError("non-public address")

    return parsed

# 请求时还应设置 timeout、限制响应大小,并根据需要 allow_redirects=False

这个示例主要用于说明检查顺序,不应被当作完整的通用防 SSRF 库。生产环境还需要考虑代理环境变量、IPv6 连接、DNS 变化、连接池复用、重定向以及响应体读取方式等因素。

八、代码审计和复现时的安全方法

在授权测试环境中,建议准备三个最小化服务:

  • 一个公网模拟资源服务,用于确认正常业务路径。
  • 一个仅绑定本机或测试网段的模拟服务,用于验证内网地址是否被阻断。
  • 一个返回重定向的测试服务,用于确认每次跳转是否重新校验。

测试时不要连接真实云元数据地址、办公网服务或生产管理接口。可以在本地容器网络中使用无敏感数据的回显服务,并观察应用日志、DNS 查询日志和出口防火墙日志。验证重点是:

检查项预期结果
公网 HTTPS 资源按业务规则正常访问
回环或私网地址在发起连接前拒绝
非 HTTPS 协议直接拒绝
非允许端口直接拒绝
重定向到受限地址在重定向处理阶段拒绝
超大响应或慢响应触发大小或时间限制

复现记录应包含入口参数、解析结果、最终连接目标、是否发生重定向、响应大小和日志时间线。这样既便于确认漏洞,也有利于后续修复回归和风险定级。

九、常见修复误区

  • 只做域名黑名单:黑名单无法覆盖未知内网域名和解析变化,优先采用明确白名单。
  • 只检查输入 IP:输入可以是域名,域名解析后的地址才是实际网络目标。
  • 只检查第一次请求:重定向后的每一跳都必须重新校验。
  • 把请求放到异步任务就认为安全:异步只改变执行时间,不改变网络权限。
  • 只依赖应用层校验:应用层可能遗漏边界情况,仍应结合出口网络隔离和防火墙策略。
  • 日志记录完整 URL:URL 可能包含令牌、签名或个人数据,应进行脱敏和长度限制。

十、从架构层降低 SSRF 影响

代码修复解决的是输入控制问题,网络架构决定了漏洞一旦遗漏后的影响范围。建议将资源抓取服务放在独立子网,默认拒绝访问内部管理网段;通过固定的出口代理统一执行域名、IP、端口和流量策略;对 DNS 请求、代理访问和异常拒绝建立审计日志。

云环境中还应检查实例角色权限、元数据服务访问策略和容器网络策略。即使业务代码存在缺陷,也不应让普通 Web 服务进程拥有读取高权限元数据或访问管理平面的能力。最小权限、网络分段和凭证短时化,往往比单点的 URL 正则更可靠。

总结

SSRF 的核心不是某个特殊 payload,而是“外部输入影响了服务端的网络边界”。审计时应从数据流出发,沿着 URL 解析、DNS、连接建立、重定向和响应处理逐层确认;修复时不能停留在字符串过滤,而要同时落实协议和端口白名单、地址范围校验、重定向控制、超时限流以及网络隔离。

对于需要长期维护的系统,建议把上述规则封装为统一的安全请求组件,禁止业务代码自行拼接和请求任意 URL,并为公网访问、受信域名访问和完全禁止外连分别提供清晰的接口。这样既能减少重复审计,也能避免同类问题在不同业务模块中反复出现。

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

0篇意见

推荐意见

没有意见。

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