跳转到帖子

背景

SSRF(Server-Side Request Forgery,服务端请求伪造)是 Web 安全里比较常见但也容易被低估的一类问题。它的本质并不复杂:应用服务端根据用户输入去发起网络请求,如果缺少边界控制,攻击者就可能借服务端的网络位置访问原本不该暴露的内部资源。

这类问题在真实项目里经常出现在以下场景:远程图片抓取、Webhook 回调、URL 预览、文件导入、PDF 渲染、第三方接口转发、云存储地址校验等。很多业务代码看起来只是“帮用户请求一个 URL”,但服务端所在网络往往比普通用户更靠近内网、元数据服务、管理接口或中间件。

本文以代码审计和防护落地为主,不提供针对真实目标的攻击步骤。示例均以本地可控环境和安全验证为前提。

典型问题场景

假设某个业务提供“根据 URL 抓取图片并保存”的功能,后端代码大致如下:

public byte[] fetchImage(String url) throws IOException {
    URL target = new URL(url);
    HttpURLConnection conn = (HttpURLConnection) target.openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    conn.setRequestMethod("GET");

    try (InputStream in = conn.getInputStream()) {
        return in.readAllBytes();
    }
}

从功能角度看,这段代码可以正常工作。但从安全角度看,至少存在几个明显问题:

  • 用户可以完全控制请求目标。
  • 没有限制协议类型,例如 file、ftp、gopher 等协议在不同语言和库中可能表现不同。
  • 没有限制访问地址范围,可能访问内网地址、回环地址、链路本地地址。
  • 没有处理 DNS 解析结果与重定向后的最终目标。
  • 没有限制响应大小,可能造成内存压力或资源消耗。

审计时优先关注的入口

做 SSRF 代码审计时,不建议只搜索“http”或“url”这类关键词,因为噪声比较大。更有效的方法是从“服务端主动发起网络请求”的 API 和业务功能入手。

常见 Java 侧关注点包括:

  • java.net.URL#openConnection
  • HttpURLConnection
  • OkHttpClient
  • Apache HttpClient
  • RestTemplate
  • WebClient
  • Jsoup.connect
  • ImageIO.read(URL)

Node.js、Python、Go 项目也有类似思路,重点不是具体库名,而是确认“请求目标是否来自用户输入或间接可控数据”。例如配置平台、任务队列、数据库字段、Webhook 地址,都可能成为间接输入点。

本地安全验证方法

验证 SSRF 风险不需要访问任何真实内网目标。可以在本机启动一个测试服务,观察后端是否会按照用户输入发起请求。

# 启动一个本地 HTTP 服务,用于观察请求
python3 -m http.server 8000

如果业务接口支持提交 URL,可以使用本地地址作为测试目标:

curl -X POST  \
  -H 'Content-Type: application/json' \
  -d '{"url":" 范围、重定向、响应大小和超时等。

技术分析:SSRF 防护为什么容易写错

很多团队会写一个简单的黑名单,例如禁止 URL 中包含 127.0.0.1、localhost、169.254。这种做法通常不可靠,原因有几个:

  • 同一个地址可能有不同表示方式,例如 IPv6、整数形式、八进制形式等。
  • 域名解析结果可能指向内网地址。
  • DNS 结果可能在校验和请求之间发生变化。
  • HTTP 30x 重定向可能把请求带到另一个地址。
  • 只检查字符串无法覆盖 URL 解析差异。

更稳妥的做法是采用白名单思路:明确允许访问哪些域名、哪些协议、哪些端口。如果业务确实需要访问任意公网地址,也要在解析后对 IP 范围做严格判断,并在每次重定向后重复判断。

推荐的防护策略

实际落地时,可以按以下顺序处理:

  • 只允许 http 和 https 协议。
  • 限制端口,例如只允许 80 和 443,或业务明确需要的端口。
  • 优先使用域名白名单,避免开放任意外部地址。
  • 解析域名后检查所有 A、AAAA 记录,拒绝内网、回环、链路本地、保留地址。
  • 禁止或严格处理重定向;如果允许重定向,每一跳都要重新校验。
  • 设置连接超时、读取超时、最大响应大小。
  • 不要把请求错误详情原样返回给前端,避免暴露内部网络信息。
  • 在网络层增加出口访问控制,让应用即使有漏洞也难以访问敏感网段。

Java 示例:基础 URL 校验

下面是一个简化版的校验逻辑,重点展示思路。生产环境还需要结合业务白名单、代理、IPv6 策略、日志审计等进一步完善。

import java.net.*;
import java.util.*;

public class UrlGuard {
    private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
    private static final Set<Integer> ALLOWED_PORTS = Set.of(80, 443);

    public static void validate(String rawUrl) throws Exception {
        URI uri = new URI(rawUrl).normalize();

        String scheme = uri.getScheme();
        if (scheme == null || !ALLOWED_SCHEMES.contains(scheme.toLowerCase())) {
            throw new IllegalArgumentException("unsupported scheme");
        }

        String host = uri.getHost();
        if (host == null || host.isBlank()) {
            throw new IllegalArgumentException("invalid host");
        }

        int port = uri.getPort();
        int effectivePort = port == -1 ? defaultPort(scheme) : port;
        if (!ALLOWED_PORTS.contains(effectivePort)) {
            throw new IllegalArgumentException("unsupported port");
        }

        InetAddress[] addresses = InetAddress.getAllByName(host);
        for (InetAddress address : addresses) {
            if (isPrivateOrReserved(address)) {
                throw new IllegalArgumentException("blocked address range");
            }
        }
    }

    private static int defaultPort(String scheme) {
        return "https".equalsIgnoreCase(scheme) ? 443 : 80;
    }

    private static boolean isPrivateOrReserved(InetAddress address) {
        return address.isAnyLocalAddress()
                || address.isLoopbackAddress()
                || address.isLinkLocalAddress()
                || address.isSiteLocalAddress()
                || address.isMulticastAddress();
    }
}

这段代码只能算基础版本。比如 IPv6 的特殊地址段、运营商 NAT 地址、保留地址、云环境元数据地址等,仍建议使用更完整的 IP 地址库或统一的安全组件来判断。

处理重定向的注意点

很多 SSRF 修复遗漏在重定向上。开发只校验了用户提交的初始 URL,但 HTTP 客户端默认跟随 301、302、307、308,最终请求地址可能已经发生变化。

更安全的方式是关闭自动重定向,由业务代码手动处理每一跳,并对新的 Location 地址重新执行完整校验。

HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);

int status = conn.getResponseCode();
if (status == 301 || status == 302 || status == 307 || status == 308) {
    String location = conn.getHeaderField("Location");
    // 解析相对地址、规范化 URL,并重新执行协议、端口、DNS、IP 范围校验
}

这里还有一个细节:Location 可能是相对路径,例如 /new-path,需要基于当前 URL 正确解析后再校验,不能简单字符串拼接。

响应大小和资源消耗

SSRF 不一定只用于访问内部地址,也可能被用来消耗服务资源。常见问题包括下载超大文件、慢速响应拖住线程、返回压缩炸弹等。

建议至少做三类限制:

  • 连接超时和读取超时。
  • 最大响应体大小,例如头像抓取限制 2MB 或 5MB。
  • Content-Type 校验,但不要只依赖响应头,必要时检查文件魔数。
private static byte[] readLimited(InputStream in, int maxBytes) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buffer = new byte[8192];
    int total = 0;
    int n;

    while ((n = in.read(buffer)) != -1) {
        total += n;
        if (total > maxBytes) {
            throw new IOException("response too large");
        }
        out.write(buffer, 0, n);
    }
    return out.toByteArray();
}

网络层兜底很重要

只靠应用层校验不够。比较成熟的做法是让应用运行环境默认不能访问敏感网段,把外联能力收敛到必要范围。

层面建议
容器网络限制 Pod 或容器访问内网管理段、元数据地址和不必要端口
安全组出方向按需开放,不要默认全放通
代理外部 HTTP 请求统一走受控代理,代理侧做域名和 IP 策略
日志记录目标域名、解析 IP、状态码、耗时、响应大小,便于排查异常

如果业务确实需要访问任意互联网 URL,统一出口代理通常比每个业务各写一套校验更稳。代理层可以集中维护地址库、黑白名单、访问审计和告警规则。

代码审计清单

实际审计时,我通常按下面的清单过一遍:

  • 是否存在用户可控 URL、Host、Callback、Webhook、Avatar、Import、Preview 等字段。
  • 是否由服务端发起请求,而不是前端浏览器直接请求。
  • 是否限制协议和端口。
  • 是否解析 DNS 并判断所有解析结果。
  • 是否处理 IPv6、回环地址、私有地址、链路本地地址、保留地址。
  • 是否关闭自动重定向,或对每一跳重新校验。
  • 是否存在二次请求,例如先校验一次 URL,后续另一个库又重新请求。
  • 是否限制响应大小和超时。
  • 错误信息是否会泄露内部地址、端口、服务 banner。
  • 运行环境是否有出网限制。

常见误区

  • 只用正则判断 URL。URL 解析有很多边界情况,正则很难覆盖完整语义。
  • 只禁止 localhost。内网地址、IPv6、DNS 解析结果同样需要处理。
  • 只在 controller 层校验。异步任务、消息消费、后台管理功能也可能触发请求。
  • 信任 Content-Type。响应头可以被服务端任意设置,关键场景要结合内容检测。
  • 忽略云环境元数据服务。云主机、容器环境中要特别关注相关地址访问控制。

总结

SSRF 的修复重点不是写几个黑名单字符串,而是建立完整的请求边界:输入规范化、协议限制、端口限制、DNS 解析校验、IP 范围判断、重定向处理、资源限制和网络层兜底。

从长期维护角度看,建议把这类能力沉淀为公共组件或统一代理,不要让每个业务模块自己实现一遍。安全问题往往不是出在“完全没防”,而是出在某个边界条件遗漏。把边界条件集中处理,后续审计和升级都会轻松很多。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.