跳转到帖子

背景

SSRF(Server-Side Request Forgery)这类问题在日常代码审计和应急排查中出现频率很高。它的危险点不在于“服务端发起了一次请求”本身,而在于请求目标、协议、重定向链路、DNS 解析结果、云环境元数据地址等因素叠加后,可能让一个普通的 URL 抓取功能变成内网探测入口。

这篇整理来自一次真实业务场景的复盘:某系统提供“远程图片抓取”和“URL 预览”能力,安全设备发现服务端对多个内网地址产生异常请求。最终确认问题并非单点漏洞,而是 URL 校验、重定向处理、DNS 解析和出网策略都存在缺口。

本文只讨论防护、审计和复现验证思路,不提供针对第三方系统的攻击操作指引。相关测试应在授权环境中进行。

问题场景

业务接口大致逻辑如下:用户提交一个 URL,后端请求该 URL,获取图片或网页标题,然后存储或展示给前端。常见代码形态包括:

POST /api/fetch-preview
Content-Type: application/json

{
  "url": "
}

看起来只是一个普通的服务端 HTTP 客户端调用,但风险点通常出现在以下位置:

  • 只用字符串前缀判断 URL 是否以 http 或 https 开头。
  • 只校验用户输入的原始域名,没有校验最终解析出的 IP。
  • 允许 HTTP 客户端自动跟随 30x 重定向。
  • 没有限制访问私网地址、回环地址、链路本地地址、云元数据地址。
  • 没有设置合理的超时、响应大小限制和协议限制。
  • 服务所在网络可以直接访问大量内网管理面。

技术分析

SSRF 防护的难点在于“看起来是外部 URL”的输入,最终可能在服务端侧变成访问内部资源的请求。这里需要重点关注几个细节。

1. URL 解析差异

不同语言、不同库对 URL 的解析行为并不完全一致。例如用户名字段、端口、编码字符、IPv6 表示法、十进制/八进制 IP 表示,都可能造成业务校验和实际请求目标不一致。

审计时不要只看这样的判断:

if (!url.startsWith("http")) {
    throw new IllegalArgumentException("invalid url");
}

这种方式无法确认协议、主机、端口和最终请求地址。更稳妥的方式是使用标准库解析 URL,并对解析结果做白名单或黑名单校验。

2. DNS 解析与重绑定风险

很多代码只判断 host 是否是某个外部域名,但实际连接前会再次进行 DNS 解析。如果解析结果变化,或者域名短时间内指向内网 IP,就可能绕过只基于域名的判断。

防护思路是:请求前解析域名,校验解析出的所有 IP;连接时尽量绑定已校验 IP;对于重定向后的新地址重复执行相同校验。

3. 自动重定向

HTTP 客户端默认跟随 301/302/307/308 重定向是常见隐患。即使初始 URL 是可信外部地址,重定向目标也可能落到内网地址或非预期协议。

建议关闭自动重定向,改为手动处理,每一跳都重新解析并校验:

// 示例:伪代码
response = httpClient.request(url, followRedirect = false)
if response.status in [301, 302, 307, 308]:
    nextUrl = resolveLocation(url, response.headers["Location"])
    validateUrl(nextUrl)
    // 限制最大跳转次数,例如不超过 3 次

4. 私网与特殊地址过滤

服务端请求应至少拒绝以下地址段:

类型示例
回环地址127.0.0.0/8、::1
私网地址10.0.0.0/8、172.16.0.0/12、192.168.0.0/16
链路本地地址169.254.0.0/16、fe80::/10
云元数据常见地址169.254.169.254
本机或未指定地址0.0.0.0、::、localhost

注意不要只过滤字符串形式的 127.0.0.1。实际解析后的 IP 才是判断依据。

关键步骤:一次安全复现与定位流程

在授权测试环境中,可以按下面流程验证业务是否存在 SSRF 风险。

1. 确认入口和出网行为

先找到所有服务端请求外部 URL 的接口,例如图片上传、头像抓取、网页预览、Webhook 回调、PDF 生成、XML 解析、文件导入等。

可在测试环境使用一个可控的 HTTP 服务观察服务端是否会发起请求:

python3 -m http.server 8000

如果业务服务器请求了该测试服务,可以从访问日志中确认来源 IP、User-Agent、请求路径和时间。

2. 验证协议和端口限制

重点确认是否只允许 http/https,是否限制端口范围。一般业务场景不应允许任意协议,也不应允许访问管理类端口。

# 只作为测试环境中的观测服务示例
nc -l 8081

如果后端允许访问任意端口,至少说明缺少端口策略。生产环境建议仅允许 80、443 或业务明确需要的端口。

3. 验证重定向处理

可以在测试环境搭一个简单服务返回 302,观察后端是否继续跟随跳转。如果会跟随,需要确认每次跳转是否重新执行目标校验。

from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header("Location", "
        self.end_headers()

HTTPServer(("0.0.0.0", 9000), H).serve_forever()

这里的 Location 仅使用公开安全域名示例。实际测试时不要指向未授权内网资产。

4. 检查响应处理限制

除了访问目标,响应处理也要关注。没有大小限制可能导致内存占用过高;没有类型检查可能把 HTML 当图片存储;没有超时可能导致线程阻塞。

建议设置:

  • 连接超时:例如 2 到 5 秒。
  • 读取超时:例如 5 到 10 秒。
  • 最大响应体:例如图片抓取限制在 5MB 或业务可接受范围。
  • Content-Type 白名单:例如 image/jpeg、image/png、image/webp。
  • 禁止压缩炸弹:对解压后大小做限制。

代码侧防护示例

下面是一个偏通用的校验思路,重点是“解析 URL、限制协议、解析 IP、过滤特殊地址、控制重定向”。不同语言实现细节不同,但流程类似。

function validateTargetUrl(inputUrl):
    url = parseUrl(inputUrl)

    if url.scheme not in ["http", "https"]:
        reject("unsupported scheme")

    if url.username or url.password:
        reject("userinfo not allowed")

    if url.port not in [80, 443, null]:
        reject("port not allowed")

    ips = dnsResolveAll(url.host)
    if ips is empty:
        reject("dns resolve failed")

    for ip in ips:
        if isPrivate(ip) or isLoopback(ip) or isLinkLocal(ip) or isMetadataAddress(ip):
            reject("forbidden address")

    return url

如果使用 Java,可以注意 HttpClient、OkHttp、Apache HttpClient 对重定向和 DNS 的处理方式;如果使用 Go,则要关注 net/http 的 CheckRedirect、DialContext;如果使用 Python requests,则需要禁用 allow_redirects 并手动处理。

# Python requests 示例:关闭自动重定向并限制超时
import requests

resp = requests.get(
    url,
    allow_redirects=False,
    timeout=(3, 5),
    headers={"User-Agent": "safe-fetcher/1.0"},
    stream=True
)

注意:上面只是请求侧片段,真正上线前还需要在请求前后都做目标校验和响应大小控制。

网络侧和平台侧加固

仅靠代码防护并不稳,尤其是历史系统较多时。更建议配合网络层策略。

  • 服务出网走统一代理,由代理执行域名、IP、端口策略。
  • 默认拒绝访问内网网段,仅对必要外部域名开放。
  • 云环境中限制元数据服务访问,优先使用 IMDSv2 等更安全机制。
  • 容器环境中避免业务 Pod 默认具备访问集群管理面的能力。
  • 对服务端异常出网建立监控,例如访问 169.254.169.254、127.0.0.1、内网网段时告警。

日志与检测建议

复盘中比较有价值的日志字段包括:

  • 用户提交的原始 URL。
  • 解析后的 scheme、host、port。
  • DNS 解析得到的 IP 列表。
  • 最终连接 IP。
  • 重定向链路。
  • 响应状态码、Content-Type、响应大小。
  • 请求耗时和异常信息。

这些字段不要直接暴露给普通用户,但应进入安全日志或审计日志。发生异常时,可以快速判断是误报、业务正常访问,还是存在被利用迹象。

常见误区

  • 只检查 URL 字符串中是否包含 localhost,这种方式很容易漏掉变体。
  • 只在入口校验一次,忽略重定向后的目标。
  • 只校验域名,不校验 DNS 解析结果。
  • 认为内网接口没有鉴权也没关系,实际上 SSRF 常常借服务端网络位置访问这些接口。
  • 依赖 WAF 规则兜底,但服务端请求链路通常不完全经过入口 WAF。

总结

SSRF 的治理重点不是写一个简单的正则,而是把“服务端可访问的目标”收敛到业务真正需要的范围内。代码层要做严格 URL 解析、协议和端口限制、IP 地址校验、重定向控制、超时和响应大小限制;网络层要默认拒绝内网访问,出网统一治理;日志层要能还原最终请求目标和重定向链路。

如果系统里存在图片抓取、网页预览、Webhook、文件导入、PDF 生成这类功能,建议优先排查。很多 SSRF 风险不是一次明显的代码错误,而是多个“看起来合理的默认行为”叠加出来的。

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

0篇意见

推荐意见

没有意见。

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