跳转到帖子

背景

SSRF(Server-Side Request Forgery)这几年在真实业务里出现频率一直不低,尤其是带有“URL 预览、图片抓取、Webhook、在线导入、PDF 渲染、远程文件同步”等能力的系统。它的问题不在于某个请求函数本身危险,而是服务端替用户访问了不可信 URL,并且访问环境往往比普通用户浏览器更敏感。

这篇文章整理一次较典型的 SSRF 排查思路,重点放在代码审计、复现验证和修复策略上。内容只讨论授权测试环境中的分析方法,不涉及未授权利用、内网探测或攻击扩展。

场景描述

某业务提供“远程图片导入”功能,用户提交一个图片 URL,后端下载图片并存入对象存储。功能看起来很常见,入口大概类似:

POST /api/image/import
Content-Type: application/json

{
  "url": "
}

后端逻辑包括三步:

  • 校验 URL 字符串格式。
  • 使用 HTTP 客户端请求该 URL。
  • 判断响应 Content-Type 和文件大小后保存。

问题出在第一步和第二步之间:代码只判断了 URL 是否以 http 或 https 开头,没有对解析后的主机、重定向、DNS 解析结果做约束。

问题代码示例

下面是一个抽象后的示例,语言以 Java 为例,其他语言中的问题本质相同:

public byte[] fetchImage(String url) throws IOException {
    if (!url.startsWith(" && !url.startsWith(" {
        throw new IllegalArgumentException("invalid scheme");
    }

    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    conn.setInstanceFollowRedirects(true);

    String contentType = conn.getContentType();
    if (contentType == null || !contentType.startsWith("image/")) {
        throw new IllegalArgumentException("not image");
    }

    return conn.getInputStream().readAllBytes();
}

这段代码有几个常见隐患:

  • 只做字符串前缀判断,未使用标准 URL 解析结果进行校验。
  • 默认跟随重定向,初始 URL 安全不代表跳转后的 URL 安全。
  • 只校验 Content-Type,无法约束服务端实际访问目标。
  • 没有限制解析出的 IP 范围,例如本地地址、内网地址、链路本地地址等。
  • 没有响应体大小上限,存在资源消耗风险。

技术分析

SSRF 排查时,不建议只盯着“能不能访问某个地址”,更重要的是梳理服务端请求链路。一个安全的远程抓取功能,至少要明确以下几个点:

  • 允许哪些协议:通常只允许 http 和 https。
  • 允许访问哪些域名:优先白名单,而不是黑名单。
  • 是否允许跳转:如果允许,跳转后的每一跳都要重新校验。
  • DNS 解析结果是否可信:需要检查解析后的 IP 是否落在禁止访问的网段。
  • 是否存在 DNS Rebinding:校验和实际连接之间不能完全割裂。
  • 请求方法、请求头、超时、最大响应大小是否受控。

其中最容易被忽略的是重定向和 DNS。很多修复只检查用户输入的第一跳 URL,但 HTTP 客户端自动跟随 302 后,真正访问的可能已经不是最初校验过的目标。

安全复现思路

在授权测试环境中,可以搭建两个本地服务来模拟“正常外部地址跳转到受限地址”的情况。这里不提供针对真实内网资产的利用步骤,只用于验证业务代码是否会跟随跳转、是否对跳转目标重新校验。

# 启动一个普通测试服务,返回 302 跳转
python3 -m http.server 8000

也可以用一个最小 Flask 服务模拟响应:

from flask import Flask, redirect

app = Flask(__name__)

@app.route('/redirect')
def r():
    return redirect(' code=302)

app.run(host='0.0.0.0', port=8000)

另一个端口作为受限目标模拟服务:

from flask import Flask

app = Flask(__name__)

@app.route('/test')
def test():
    return 'local service', 200, {'Content-Type': 'text/plain'}

app.run(host='127.0.0.1', port=9000)

如果业务后端请求了第一个地址,并且第二个本地服务收到请求,就说明当前代码在重定向场景下存在访问边界失控问题。实际验证时应只在本机、测试容器或授权环境内进行。

关键审计点

代码审计时可以从“输入点”和“请求点”两个方向找。

1. 常见输入点

  • 图片、头像、附件的远程导入。
  • URL 预览、链接解析、网页截图。
  • Webhook 回调地址配置。
  • 第三方数据源导入。
  • PDF/HTML 渲染中的远程资源加载。
  • XML、Markdown、富文本解析中的外部资源引用。

2. 常见请求函数

  • Java:HttpURLConnection、Apache HttpClient、OkHttp、RestTemplate、WebClient。
  • Go:net/http、http.Client。
  • Python:requests、urllib、aiohttp。
  • Node.js:axios、node-fetch、request、got。
  • PHP:curl、file_get_contents、Guzzle。

审计时建议全文检索这些请求库,再反向追踪 URL 参数来源。如果 URL 可被用户直接或间接控制,就需要继续确认是否有完整的目标校验。

修复思路

SSRF 的修复不要依赖单点判断,建议做成统一的安全请求组件,业务层禁止直接调用通用 HTTP 客户端访问用户传入 URL。

一个相对稳妥的策略如下:

  • 只允许 http 和 https。
  • 优先使用域名白名单,确实无法白名单时再做严格网段限制。
  • 禁止访问本地地址、私有地址、链路本地地址、保留地址、组播地址等。
  • 关闭自动重定向,手动处理跳转,并对每一跳重复校验。
  • 限制最大跳转次数,例如不超过 3 次。
  • 设置连接超时、读取超时、最大响应体大小。
  • 限制请求方法和请求头,避免带入内部凭据。
  • 下载类功能先落临时文件,再做类型和大小校验。

IP 范围校验示例

下面示例只展示思路,生产环境建议使用成熟 IP/CIDR 库,避免 IPv6、IPv4 映射地址、编码变体等细节处理不完整。

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

仅使用 isSiteLocalAddress 并不够,一些保留网段、特殊地址段、IPv6 场景还需要额外覆盖。实际工程里更建议维护一组禁止 CIDR,并统一做判断。

重定向处理示例

重点是不要让 HTTP 客户端自动跟随跳转,而是在每一跳前做 URL 和解析结果校验:

int maxRedirects = 3;
URL current = new URL(inputUrl);

for (int i = 0; i <= maxRedirects; i++) {
    validateUrl(current);

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

    int code = conn.getResponseCode();
    if (code == 301 || code == 302 || code == 303 || code == 307 || code == 308) {
        String location = conn.getHeaderField("Location");
        if (location == null) {
            throw new IOException("empty redirect location");
        }
        current = new URL(current, location);
        continue;
    }

    if (code != 200) {
        throw new IOException("unexpected status: " + code);
    }

    // 后续再做 Content-Type、大小和文件内容校验
    break;
}

这里的 validateUrl 应该包含 scheme 校验、host 校验、DNS 解析、IP 范围判断等逻辑。需要注意,DNS 校验和实际连接之间仍可能存在竞态风险,高安全要求场景可以考虑通过自定义 DNS 解析结果绑定连接目标,或者把远程抓取任务放到隔离网络环境中执行。

防护架构建议

如果业务确实需要频繁访问用户提供的外部 URL,仅靠代码层校验还不够,建议配合网络层隔离:

  • 将抓取服务独立部署,避免与核心业务服务共用运行环境。
  • 抓取服务所在网络禁止访问内部管理网段、元数据服务和数据库网段。
  • 通过出口代理统一访问外网,并在代理层做域名、IP、端口策略。
  • 对异常访问目标、失败率、响应大小做日志监控。
  • 云环境中限制实例元数据服务访问,或使用 IMDSv2 等更安全配置。

工程上比较推荐“业务服务提交任务,隔离抓取服务执行,结果写入对象存储”的模式。这样即使抓取逻辑出现遗漏,影响面也会小很多。

日志与检测

排查历史风险时,可以从访问日志和应用日志里找一些特征:

  • 用户提交的 URL host 为 IP 地址而非域名。
  • URL 中出现 localhost、127.0.0.1、0.0.0.0、::1 等本地标识。
  • 大量访问非标准端口。
  • 短时间内同一账号提交大量不同 URL。
  • 重定向链路异常,第一跳和最终访问 host 差异较大。
  • 下载失败率明显高于正常用户。

日志中建议记录规范化后的 URL、最终访问 host、解析 IP、状态码、响应大小、耗时、重定向次数等字段。注意不要记录敏感响应体。

容易踩坑的点

  • 只做字符串黑名单:URL 编码、大小写、IPv6、十进制/八进制 IP 表示都可能导致判断偏差。
  • 只校验第一跳:自动重定向会绕过初始校验边界。
  • 只看 Content-Type:Content-Type 是响应头,不代表访问目标安全。
  • 忽略 IPv6:很多拦截规则只覆盖 IPv4。
  • 允许任意端口:远程抓取通常只需要 80 和 443,其他端口应谨慎开放。
  • 复用业务凭据:服务端请求外部地址时不应携带内部认证头或 Cookie。

总结

SSRF 的本质是“服务端替用户访问了不可信目标”。修复时不能只补一个正则或黑名单,而要围绕协议、域名、DNS、IP、重定向、网络出口和运行环境做完整约束。

从实际经验看,最有效的组合是:统一安全请求组件、关闭自动重定向并逐跳校验、严格限制目标范围、抓取服务网络隔离、日志可观测。这样即使某一层判断有遗漏,也不至于直接扩大成高风险问题。

建议把 SSRF 防护沉到基础组件里,而不是让每个业务接口各写一套校验。安全边界越分散,后续越难维护。
代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

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