跳转到帖子

背景

SSRF(Server-Side Request Forgery)这类问题在代码审计和漏洞复现里很常见,但很多复盘文章容易停留在“传一个 URL,服务端帮忙请求”这个层面。实际项目里,SSRF 往往藏在图片抓取、Webhook 校验、文件导入、URL 预览、PDF 生成、对象存储回源等功能中,表面看是业务能力,底层却是服务端主动发起网络请求。

这篇文章整理一个偏真实的审计思路:如何从入口定位 SSRF,如何判断影响面,如何做安全复现,以及如何把修复措施落到代码和网络层。文中不会涉及内网探测、云元数据读取等攻击引导,只关注漏洞分析和防护实现。

典型场景

某业务系统提供“远程图片上传”功能,用户输入一个图片 URL,后端下载图片后保存到对象存储。简化后的调用链大致如下:

Controller: /api/image/fetch
  - 接收参数 imageUrl
Service: ImageFetchService.fetch(imageUrl)
  - 校验后缀是否为 jpg/png/gif
  - 使用 HTTP Client 下载资源
  - 判断 Content-Type
  - 保存文件

从业务上看,这个功能合理;从安全上看,风险点在于服务端会根据用户输入访问任意地址。如果校验只做字符串后缀或 Content-Type 判断,就容易出现 SSRF。

问题定位

审计这类功能时,我一般先找“服务端主动请求外部资源”的代码,而不是直接搜索 SSRF 关键词。常见入口包括:

  • 图片、头像、附件的 URL 上传或搬运。
  • 文章链接预览、OpenGraph 信息抓取。
  • Webhook 回调地址配置和测试。
  • 在线导入远程配置、远程模板、远程 CSV。
  • PDF/截图服务中引用外部资源。
  • 服务端代理接口,例如 /proxy?url=。

代码层面可以重点搜索这些 API 或类名:

// Java 常见关键字
RestTemplate
WebClient
HttpClient
OkHttpClient
URLConnection
URI.create
URL.openConnection

// Node.js 常见关键字
axios(
fetch(
request(
got(
http.get
https.get

// PHP 常见关键字
file_get_contents(
curl_exec(
fopen(
stream_context_create(

如果发现请求目标可由用户输入控制,就需要继续检查三个点:是否限制协议、是否限制目标地址、是否处理跳转。

不安全实现示例

下面是一个常见的简化版 Java 实现。问题不在于使用了某个具体 HTTP 客户端,而在于对 URL 的信任边界不清晰。

public byte[] fetchImage(String imageUrl) {
    if (!imageUrl.endsWith(".jpg") && !imageUrl.endsWith(".png")) {
        throw new IllegalArgumentException("unsupported file type");
    }

    ResponseEntity<byte[]> resp = restTemplate.getForEntity(imageUrl, byte[].class);
    String contentType = resp.getHeaders().getFirst("Content-Type");

    if (contentType == null || !contentType.startsWith("image/")) {
        throw new IllegalArgumentException("invalid image");
    }

    return resp.getBody();
}

这段代码的问题主要有:

  • 仅用后缀判断资源类型,URL 路径和真实响应没有强绑定。
  • 没有限制协议,可能接受非预期 scheme。
  • 没有限制目标主机,用户可以让服务端访问任意地址。
  • 没有处理 DNS 解析结果,无法防止域名解析到保留地址。
  • 没有限制重定向,初始 URL 合法但跳转目标可能不合法。
  • 没有设置合理的超时和大小限制,可能引发资源消耗问题。

安全复现建议

在测试环境里复现 SSRF,不建议直接对生产内网、云元数据地址、第三方目标做请求。更稳妥的方式是搭一个可控的本地 HTTP 服务,观察服务端是否发起了请求。

# 在测试机上启动一个简单 HTTP 服务
python3 -m http.server 8000

然后在业务功能里填写测试 URL,例如:

 HTTP 服务日志里出现来自业务服务器的访问记录,就说明该功能存在服务端代请求行为。接下来再验证安全策略是否完善,比如是否允许非白名单域名、是否跟随跳转、是否处理异常 Content-Type 等。

如果需要验证重定向场景,可以在测试环境里写一个只跳到另一测试域名的接口,确认后端是否会在跳转后重新做地址校验。这里的关键不是“跳到哪里”,而是确认“每一次最终请求前是否都执行了同一套安全校验”。

技术分析:SSRF 防护不能只靠黑名单

很多修复会写成“禁止访问 127.0.0.1、localhost、内网网段”,这比完全不处理要好,但并不可靠。原因包括:

  • 地址表示形式很多,IPv4、IPv6、整数、八进制、十六进制等都可能造成解析差异。
  • localhost 只是一个名称,不等同于所有本地地址。
  • 域名可以通过 DNS 解析到保留地址。
  • DNS 结果可能发生变化,存在 TOCTOU 问题。
  • HTTP 重定向会让实际访问目标和初始输入不同。

更推荐的策略是“白名单 + 解析后校验 + 禁止非预期协议 + 每次跳转重新校验 + 网络层兜底”。如果业务能明确只允许访问公司自有 CDN 或指定对象存储域名,就不要开放任意 URL。

修复思路

1. 限制协议

只允许 http 和 https,拒绝 file、gopher、ftp、jar 等非业务必需协议。不同语言和 HTTP 客户端对 scheme 的支持不完全一致,最好在进入请求库前就做显式判断。

URI uri = URI.create(inputUrl);
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
    throw new IllegalArgumentException("unsupported scheme");
}

2. 使用域名白名单

白名单应基于业务域名,而不是用户可随意填写的完整 URL。比如只允许:

  • img.example.com
  • static.examplecdn.com
  • bucket.example-oss.com

注意不要用简单的 contains 判断域名,否则 example.com.evil.test 这类字符串会误判。应使用标准 URI 解析结果中的 host,并做精确匹配或受控的后缀匹配。

private static final Set<String> ALLOWED_HOSTS = Set.of(
    "img.example.com",
    "static.examplecdn.com"
);

private boolean isAllowedHost(String host) {
    if (host == null) return false;
    String normalized = host.toLowerCase(Locale.ROOT);
    return ALLOWED_HOSTS.contains(normalized);
}

如果确实要允许子域名,建议明确后缀边界:

private boolean isAllowedSubdomain(String host) {
    if (host == null) return false;
    String h = host.toLowerCase(Locale.ROOT);
    return h.equals("examplecdn.com") || h.endsWith(".examplecdn.com");
}

3. 解析后校验 IP

即使启用了域名白名单,也建议对解析结果做保留地址过滤,避免 DNS 配置错误或供应链问题带来风险。常见需要拒绝的地址包括:

  • 本地回环地址。
  • 私有地址段。
  • 链路本地地址。
  • 组播地址。
  • 未指定地址。
  • IPv6 本地地址。
private boolean isSafeAddress(InetAddress address) {
    return !(address.isAnyLocalAddress()
        || address.isLoopbackAddress()
        || address.isLinkLocalAddress()
        || address.isSiteLocalAddress()
        || address.isMulticastAddress());
}

这里要注意,Java 的 isSiteLocalAddress 覆盖常见私有地址,但实际环境里还可以结合网关策略补充更严格的网段控制。应用层校验不是网络边界的替代品。

4. 处理重定向

自动跟随重定向很容易绕过初始校验。建议关闭 HTTP 客户端的自动跳转,手动处理 3xx 响应,并对 Location 指向的新 URL 重新执行同一套校验。

// 伪代码:每次请求前都校验 URL
String currentUrl = inputUrl;
for (int i = 0; i < 3; i++) {
    validateUrl(currentUrl);
    HttpResponse resp = httpClient.get(currentUrl, followRedirects = false);

    if (resp.statusCode() / 100 == 3) {
        String location = resp.header("Location");
        currentUrl = resolveRedirect(currentUrl, location);
        continue;
    }

    return resp;
}
throw new IllegalArgumentException("too many redirects");

重定向次数也应限制,通常 3 次以内足够满足业务需求。

5. 限制响应大小和超时

远程抓取接口还容易被用来消耗资源。建议设置连接超时、读取超时、最大响应大小,并避免一次性把未知大小响应读入内存。

connectTimeout: 2s
readTimeout: 5s
maxResponseSize: 5MB
maxRedirects: 3

下载文件时可以边读边计数,超过阈值立即中断。仅依赖 Content-Length 不够,因为对端可以不返回或返回不可信值。

6. 内容校验不要只看 Content-Type

Content-Type 是服务端声明,不是可靠的文件类型证明。图片类业务可以读取文件头做基础校验,必要时用图片库实际解码一次。这样可以避免把 HTML、脚本、压缩包等非预期内容保存为图片。

boolean looksLikePng(byte[] header) {
    return header.length >= 8
        && header[0] == (byte)0x89
        && header[1] == 0x50
        && header[2] == 0x4E
        && header[3] == 0x47;
}

如果后续文件会被浏览器直接访问,还要配合对象存储的 Content-Disposition、Content-Type 固定化、独立域名隔离等策略,避免上传内容引发其他类型风险。

防护落地示例

一个相对完整的校验流程可以设计成:

validate(inputUrl):
  1. 解析 URI,拒绝解析失败的输入
  2. 只允许 http/https
  3. 提取 host,禁止空 host
  4. 校验 host 是否在业务白名单
  5. DNS 解析 host
  6. 校验所有解析结果都不属于保留地址
  7. 发起请求时禁用自动重定向
  8. 如遇 3xx,解析 Location 后回到第 1 步
  9. 限制超时、响应大小、文件类型

需要强调一点:如果应用在请求前解析了一次域名,而 HTTP 客户端请求时又自行解析一次,理论上仍可能存在解析结果不一致的问题。更严谨的做法是在网络出口层做限制,只允许访问必要的外部服务。应用层负责减少误用,网络层负责兜底。

网络层兜底

SSRF 的影响大小,很大程度取决于服务端所在网络能访问什么。生产环境建议配合以下措施:

  • 业务服务默认禁止访问内网敏感网段,只开放必要外联。
  • 通过 egress proxy 统一外联,并在代理侧做域名白名单。
  • 云环境里对元数据服务启用更严格的访问控制和实例角色最小权限。
  • 容器环境限制 Pod 出站访问,避免默认全通。
  • 对异常外联做日志审计和告警。

在实际事件里,单纯修代码经常不够。代码遗漏、历史接口、第三方组件都有可能再次引入 SSRF。网络出口策略能显著降低问题扩大化的概率。

审计检查清单

检查项建议
用户是否可控 URL确认参数、配置、数据库字段是否能影响服务端请求目标
协议限制只允许业务需要的 http/https
域名限制优先白名单,避免 contains 这类字符串判断
DNS 解析解析后校验 IP,拒绝保留地址
重定向关闭自动跳转,手动校验每一个 Location
资源限制设置超时、大小、重定向次数
内容校验不要只信 Content-Type,必要时校验文件头或解码
网络出口服务侧最小出站权限,代理统一管控
日志审计记录目标域名、状态码、耗时、拒绝原因

常见误区

  • 只过滤 localhost:本地地址不只有 localhost。
  • 只过滤 127.0.0.1:还有 IPv6、其他回环表示和地址变形。
  • 只判断 URL 后缀:路径不代表响应内容。
  • 只信 Content-Type:响应头可由对端任意设置。
  • 认为白名单域名一定安全:DNS 配置、CNAME 链和供应链也需要关注。
  • 修了一个接口就结束:类似逻辑通常在多个业务模块重复出现。

日志与监控建议

SSRF 防护不只是拒绝请求,还应该能发现异常使用。建议记录以下字段:

  • 调用接口和业务用户标识。
  • 原始输入 URL。
  • 解析后的 host 和 IP。
  • 是否发生重定向,以及最终 URL。
  • 拒绝原因或成功状态码。
  • 请求耗时和响应大小。

日志里注意脱敏,不要记录敏感 token 或完整鉴权参数。对于短时间大量访问不同域名、频繁触发拒绝规则、请求耗时异常的行为,可以做基础告警。

总结

SSRF 的核心不是某个 payload,而是“服务端把用户输入当成网络请求目标”。审计时先找服务端代请求入口,再看协议、域名、解析、重定向和资源限制。修复时不要依赖黑名单,优先采用业务白名单,并把校验放到每一次真实请求前。

从长期治理角度看,建议把远程抓取能力封装成统一组件,业务方不要各写各的 HTTP 请求逻辑。组件层负责 URL 校验、跳转处理、超时大小限制、日志审计;网络层再做出口白名单。这样比在每个接口里临时补几行判断要稳定得多,也更容易通过代码审计和安全测试。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.