跳转到帖子

背景

SSRF(Server-Side Request Forgery)这类问题在代码审计和漏洞复现里很常见,但真正处理起来并不只是“禁用内网地址”这么简单。很多业务会天然需要服务端发起请求,例如:远程图片抓取、Webhook 回调、在线文档预览、URL 健康检查、第三方接口转发等。如果只做简单黑名单,通常很容易被各种地址表示法、跳转链、DNS 解析变化绕过去。

这篇文章整理一个偏工程化的 SSRF 分析方法,重点放在安全验证、漏洞定位和加固落地。内容不会展开攻击性利用,只讨论如何识别风险、复现边界和修复设计。

典型场景

下面这些接口在审计中需要重点关注:

  • 根据用户提交的 URL 抓取图片、头像、封面、文件。
  • Webhook、Callback、OpenAPI 回调地址配置。
  • 在线代理、站点截图、URL 预览、网页转 PDF。
  • 后端请求第三方资源,并将响应内容返回给前端。
  • 文件导入功能支持 HTTP/HTTPS 远程地址。

一个常见的风险接口可能长这样:

POST /api/fetch-image HTTP/1.1
Content-Type: application/json

{
  "url": "
}

如果服务端直接拿这个 url 发请求,并把结果返回或保存,就需要考虑 SSRF 风险。

问题本质

SSRF 的核心不是“请求了一个 URL”,而是“用户可控输入影响了服务端的网络访问目标”。服务端一般处在更高权限的网络位置,可能能访问到公网用户无法访问的内部服务、云厂商元数据接口、管理面板、Redis、Elasticsearch、Kubernetes API 等。

从代码角度看,危险模式通常是:

// 伪代码,仅用于说明风险模式
url := request.json["url"]
resp := httpClient.Get(url)
return resp.Body

这段代码的问题不在于使用了 HTTP 客户端,而在于没有对访问目标、协议、解析结果、跳转行为和响应体做任何约束。

技术分析

1. URL 解析不一致

不同语言、不同库对 URL 的解析行为不完全一致。业务代码校验时使用一个解析器,真正请求时 HTTP 客户端又使用另一个解析器,就可能产生判断差异。审计时要确认校验和请求是否基于同一套解析结果。

建议至少检查以下字段:

  • scheme:只允许 http、https。
  • host:必须存在,且不能是空值。
  • port:只允许业务必要端口,例如 80、443。
  • userinfo:不建议允许,例如 URL 中的特殊字符、编码、大小写、尾点域名、IPv6 表示等情况。修复时不要依赖字符串 contains、startsWith 这类简单判断。

    2. DNS 解析与地址校验

    只校验域名字符串是不够的。安全判断应该发生在 DNS 解析之后,对解析得到的 IP 地址进行分类检查。需要拒绝的地址范围一般包括:

    • loopback:127.0.0.0/8、::1。
    • private:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。
    • link-local:169.254.0.0/16、fe80::/10。
    • multicast、reserved、unspecified。
    • 云元数据服务常见地址,例如 169.254.169.254。

    另外要注意 DNS Rebinding。也就是第一次解析通过校验,实际请求时又解析到内部地址。稳妥做法是:由应用自己解析域名并校验 IP,然后建立连接时绑定到已校验的 IP,同时保留原始 Host/SNI 的业务需求;或者在网络层做 egress 策略,避免应用层校验成为唯一防线。

    3. 跳转链风险

    很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只检查初始 URL,不检查跳转后的 Location,仍然存在风险。建议禁用自动跳转,手动处理每一次 Location,并对新的 URL 重复完整校验。

    // Go 示例:禁用自动跳转,避免只校验首个 URL
    client := &http.Client{
        Timeout: 5 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }

    如果业务确实需要跳转,建议限制最大跳转次数,例如不超过 3 次,并要求每次跳转后的 scheme、host、解析 IP 都通过校验。

    4. 协议与客户端能力

    SSRF 不一定只发生在 HTTP。部分库或组件支持 file、gopher、ftp、dict、jar 等协议,或者在某些封装层里会自动处理特殊协议。实际审计时要看最终调用的客户端支持什么,不要只看接口名。

    一般建议:

    • 明确只允许 http 和 https。
    • 拒绝非标准端口,除非业务确实需要。
    • 不要把用户输入直接传给支持多协议的通用下载器。
    • 对响应类型和大小做限制,避免被当成内网探测或大文件下载通道。

    关键步骤:一次安全复现与确认流程

    为了避免测试行为影响线上环境,建议在本地或测试环境搭建最小复现。流程如下:

    1. 定位数据流

    先确认用户输入从入口到服务端请求的位置。代码审计时重点搜索:

    curl_exec
    file_get_contents
    fsockopen
    requests.get
    urllib.request
    http.Get
    RestTemplate
    WebClient
    OkHttpClient
    HttpClient

    同时关注图片处理、导入导出、回调配置等业务代码,不要只搜“url”。很多变量名可能是 avatar、callback、endpoint、source、remotePath。

    2. 在测试环境验证外连能力

    可以准备一个自己控制的测试 HTTP 服务,用于确认服务端是否会访问用户提交的地址。这里只做外连确认,不进行内网探测。

    python3 -m http.server 8080

    提交测试 URL 后观察测试服务日志。如果能看到服务端请求,说明入口具备服务端请求能力,后续需要检查限制是否充分。

    3. 验证过滤边界

    在授权测试环境里,可以围绕以下点做边界验证:

    • 是否限制协议,只允许 http/https。
    • 是否允许跳转,跳转后的地址是否重新校验。
    • 是否校验 DNS 解析后的 IP。
    • IPv4、IPv6、域名尾点、大小写、编码后 URL 是否处理一致。
    • 是否限制响应大小、超时时间、Content-Type。

    这里的重点是验证防护逻辑是否完整,而不是扩大影响面。生产环境不建议做内网地址访问测试。

    加固方案

    1. 优先使用白名单

    如果业务场景固定,例如只允许访问企业自己的对象存储域名、固定第三方 API 域名,白名单是最可靠的方式。不要用“禁止 127.0.0.1、localhost”这类黑名单作为主要防线。

    // 伪代码:基于业务白名单校验 host
    allowedHosts := map[string]bool{
        "img.example-cdn.com": true,
        "api.partner.example": true,
    }
    
    if !allowedHosts[parsed.Hostname()] {
        return error("host not allowed")
    }

    如果必须允许用户输入任意公网 URL,则需要组合使用 URL 规范化、DNS 解析校验、跳转校验、网络出口限制。

    2. 解析后校验 IP 范围

    校验逻辑应基于解析后的 IP,而不是原始字符串。下面是一个偏思路化的 Go 示例:

    func isBlockedIP(ip net.IP) bool {
        if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() {
            return true
        }
        // 额外阻断云元数据地址
        if ip.String() == "169.254.169.254" {
            return true
        }
        return false
    }

    实际项目里还要补充 IPv6、保留地址段和公司内部网段。不要只判断字符串前缀。

    3. 控制 HTTP 客户端行为

    • 设置较短超时,例如连接超时 2 秒、整体超时 5 秒。
    • 禁用自动跳转,或对每次跳转重新校验。
    • 限制最大响应体大小,例如 5MB。
    • 限制 Content-Type,例如只允许 image/png、image/jpeg。
    • 禁止携带内部凭据、Cookie、Authorization 头。
    • 不要把响应体原样返回给用户,避免变成代理通道。
    // Go 示例:限制响应体大小
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    limited := io.LimitReader(resp.Body, 5*1024*1024)
    body, err := io.ReadAll(limited)

    4. 网络层兜底

    应用层校验容易因为边界情况遗漏,所以建议配合网络层策略:

    • 容器或主机层限制出站访问,只允许访问必要公网地址。
    • 云环境中阻断对元数据服务的非必要访问。
    • 使用独立的下载服务,放在低权限网络区域。
    • 对服务端主动外连建立审计日志,包括目标域名、解析 IP、状态码、耗时。

    在 Kubernetes 环境,可以使用 NetworkPolicy 限制 Pod 出站访问。具体策略要结合 CNI 能力和业务依赖设计,不能只写一条“允许全部”。

    代码审计检查清单

    检查项关注点建议
    URL 来源是否由用户完全可控优先改为白名单或后端固定配置
    协议限制是否允许 file、ftp 等非 HTTP 协议只允许 http/https
    DNS 校验是否只判断域名字符串解析后校验 IP 分类
    跳转处理是否默认跟随 30x禁用自动跳转或逐跳校验
    响应限制是否无限读取响应体限制大小、类型和超时
    凭据泄露是否转发 Cookie、Token外连请求使用干净 Header
    网络出口服务是否能访问内网敏感段通过防火墙或 NetworkPolicy 兜底

    常见误区

    • 只过滤 localhost。实际还有 127.0.0.1、::1、内网段、link-local、各种域名解析情况。
    • 只在前端校验 URL。服务端必须独立校验。
    • 只校验第一次请求。跳转后的地址同样需要校验。
    • 认为 HTTPS 就安全。HTTPS 只能保证传输安全,不代表目标地址可信。
    • 把 SSRF 当成单点漏洞修补。更合理的是把外连能力当成高风险能力统一治理。

    事件复盘建议

    如果已经发现 SSRF 风险,复盘时建议关注三条线:

    • 影响面:哪些接口存在服务端外连能力,是否可被未授权或低权限用户触发。
    • 可达性:相关服务能访问哪些内部网段、云元数据、管理接口。
    • 数据暴露:接口是否会返回响应体、响应头、错误信息或时间差。

    日志侧建议检索异常目标地址、短时间大量不同 URL、访问失败率异常、非业务域名请求等。对于历史日志不足的系统,至少在修复后补充外连审计,方便后续追踪。

    总结

    SSRF 的治理重点不是写一个复杂正则,而是建立完整的访问目标控制链路:输入规范化、协议限制、白名单优先、DNS 解析后校验、跳转逐次校验、HTTP 客户端收敛、网络出口兜底。单靠某一层很难覆盖所有边界。

    从长期维护角度看,建议把“服务端主动访问外部 URL”抽象成统一组件,业务侧不要各自实现下载逻辑。统一组件里沉淀校验、日志、超时、响应大小限制和安全策略,后续审计和加固都会轻松很多。

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

0篇意见

推荐意见

没有意见。

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