跳转到帖子

URL 预览、网页截图、Markdown 远程图片解析等功能,通常需要由服务端主动访问用户提交的地址。它们看起来只是普通的网络请求,但如果缺少严格的目标校验和出站访问控制,就可能演变为服务端请求伪造(SSRF)。

SSRF 的风险不只在于“能不能访问内网”,更在于应用本身往往拥有比普通用户更高的网络可见性和身份权限。一次看似简单的 URL 请求,可能触及内部管理面板、服务发现接口、容器网络或云环境的实例身份服务。因此,审计这类功能时,不能只检查黑名单是否完善,而应从解析、解析结果校验、连接建立、重定向、DNS 变化以及网络出口策略等多个层面建立完整边界。

一、问题场景与审计目标

假设业务提供了一个 URL 预览接口:

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

{
  'url': '
}

服务端收到请求后,会抓取页面标题、摘要和缩略图,再将结果返回给前端。最初的实现可能类似下面这样:

import requests


def fetch_preview(url):
    response = requests.get(url, timeout=5)
    return response.text[:4096]

这段代码的主要问题不是使用了某个特定 HTTP 库,而是把用户输入直接交给了网络客户端。攻击者如果控制目标地址,服务端就可能替其访问原本不应暴露给用户的网络资源。

审计目标应明确为以下几点:

  • 确认服务端是否会访问用户完全可控的地址。
  • 确认是否限制了协议、端口、目标地址类型和重定向行为。
  • 确认域名解析结果是否经过校验,以及是否存在 DNS 解析结果变化问题。
  • 确认请求是否携带了内部凭据、Cookie、代理头或其他敏感信息。
  • 确认网络层是否有默认拒绝的出站策略。
  • 确认错误信息和响应内容是否会造成内部拓扑泄露。

二、先定位真实的请求链路

代码审计时,建议从用户输入开始向下追踪,而不是只搜索 requests.get 或 HttpClient。实际项目中,URL 可能经过多层封装:

controller 参数
    -> 业务服务
    -> URL 清洗器
    -> 重定向处理器
    -> HTTP 客户端
    -> 代理或网关
    -> 目标服务器

重点关注以下代码特征:

  • 从请求参数、消息队列、数据库或模板中读取 URL。
  • 调用 HTTP、HTTPS、FTP、file 等协议处理器。
  • 默认开启重定向,或手动跟随 Location 响应头。
  • 先通过 DNS 解析进行检查,再把原始域名交给 HTTP 客户端重新解析。
  • 将用户输入拼接到代理地址、抓取器命令或浏览器自动化参数中。
  • 把服务端环境变量、默认请求头或内部 Cookie 自动带到目标请求。

除了审查入口,还应确认任务是否异步执行。异步任务容易被忽略,因为接口层可能只做了格式校验,真正的请求发生在后台 Worker、渲染服务或独立的截图容器中。漏洞验证和修复必须覆盖完整调用链。

三、解析与校验为什么容易失效

很多修复停留在字符串层面,例如判断 URL 是否以某个域名开头,或者简单拦截某些内网地址。这类方案通常不可靠,原因是 URL 解析存在多个边界问题。

第一,域名、端口、用户名密码、路径和片段属于不同组成部分。把字符串前缀当成主机名判断,可能误把相似域名当成可信域名。第二,IPv4、IPv6、十进制或十六进制表示、尾随点、大小写和 IDNA 域名都可能导致不同组件产生不同的解析结果。第三,重定向可能把一个看似可信的地址引向另一个不可信地址。

因此,校验顺序应尽量统一:

  • 使用成熟 URL 解析器解析一次,并拒绝无法规范化的输入。
  • 只允许业务明确需要的协议,通常仅允许 HTTPS,必要时再开放 HTTP。
  • 拒绝用户名、密码、片段和非必要端口。
  • 提取规范化后的主机名,并进行 IDNA 处理。
  • 解析所有 A 和 AAAA 记录,对每个结果进行地址分类。
  • 拒绝回环地址、链路本地地址、私有地址、保留地址、组播地址和未指定地址。
  • 建立连接时使用已经校验过的解析结果,避免再次按原始域名解析。
  • 每次重定向都重新执行完整校验,而不是只校验首次 URL。

四、一个更稳妥的防护实现

下面示例使用 Python 展示防护思路。它是一个简化版本,适合说明审计重点,不应直接替代经过充分测试的生产级网络组件。

import ipaddress
import socket
from urllib.parse import urlsplit

ALLOWED_SCHEMES = {'https'}
ALLOWED_PORTS = {443}


def is_public_address(address):
    ip = ipaddress.ip_address(address)
    return not (
        ip.is_private or
        ip.is_loopback or
        ip.is_link_local or
        ip.is_multicast or
        ip.is_reserved or
        ip.is_unspecified
    )


def resolve_public_host(host, port):
    results = socket.getaddrinfo(
        host,
        port,
        type=socket.SOCK_STREAM
    )
    addresses = sorted({item[4][0] for item in results})
    if not addresses:
        raise ValueError('host has no address')
    if not all(is_public_address(item) for item in addresses):
        raise ValueError('destination is not allowed')
    return addresses


def validate_url(raw_url):
    parsed = urlsplit(raw_url)
    if parsed.scheme.lower() not in ALLOWED_SCHEMES:
        raise ValueError('scheme is not allowed')
    if not parsed.hostname:
        raise ValueError('host is required')
    if parsed.username or parsed.password:
        raise ValueError('userinfo is not allowed')
    if parsed.port not in ALLOWED_PORTS:
        raise ValueError('port is not allowed')
    host = parsed.hostname.rstrip('.').lower()
    addresses = resolve_public_host(host, parsed.port)
    return parsed, addresses

这个示例仍然有一个工程上的关键问题:仅在校验阶段解析 DNS,并不能天然保证后续连接一定使用同一批地址。如果 HTTP 客户端在真正建立连接时再次解析域名,解析结果可能发生变化。更稳妥的做法是使用支持固定目标地址连接、同时保留正确 Host 和 TLS SNI 的客户端封装,或者把出站访问放入独立的代理服务中,由代理统一完成解析和连接策略。

五、重定向、DNS 变化与连接复用

SSRF 审计中最容易遗漏的是“初始地址安全,但后续请求不安全”的情况。

重定向方面,建议默认关闭自动重定向。若业务必须跟随重定向,应限制最大跳转次数,并对每个 Location 目标重新执行协议、端口、域名和 IP 校验。相对路径、绝对路径、跨协议跳转都应纳入测试。

DNS 方面,不要只在应用层调用一次解析函数就认为完成了防护。常见的风险包括:

  • 校验阶段解析到公网地址,连接阶段解析到其他地址。
  • A 记录安全,但 AAAA 记录指向不允许的网络。
  • 域名背后的多个地址中只有一个属于内网。
  • 代理、连接池或底层解析器绕过了应用层的检查。

连接复用也需要注意。连接池如果按照域名复用连接,可能让一次已经建立的连接被后续请求继续使用。对于高风险抓取功能,应限制连接池生命周期、单请求连接数和目标主机范围,并记录最终连接的 IP、端口和证书信息。

六、请求头、响应体与资源消耗

即使目标地址校验正确,服务端请求本身仍可能泄露信息。建议明确构造请求头,而不是把用户请求头整体转发:

SAFE_HEADERS = {
    'User-Agent': 'PreviewFetcher/1.0',
    'Accept': 'text/html,application/xhtml+xml',
    'Accept-Language': 'zh-CN,zh;q=0.8'
}

通常不应向外部目标转发以下内容:

  • 用户浏览器的 Cookie、Authorization 和代理认证信息。
  • 内部服务使用的追踪头、管理头和调试头。
  • 可能包含租户标识、内部主机名或用户隐私的自定义头。

响应处理也不能只限制返回字符串长度。应同时限制响应体总大小、响应头大小、读取时间、连接超时、下载速度和内容类型。对压缩响应要防止解压后体积异常增长;对 HTML、SVG、PDF 和图片解析器要单独设置资源上限,避免通过解析过程触发新的网络请求或消耗大量 CPU。

MAX_BODY = 2 * 1024 * 1024
CONNECT_TIMEOUT = 3
READ_TIMEOUT = 5
MAX_REDIRECTS = 3

七、在隔离环境中验证修复

验证 SSRF 修复时,不需要也不应直接探测真实生产内网。可以在本地构造多个明确用途的测试服务:

  • 公网模拟服务:返回固定的 HTML 和重定向响应。
  • 禁止地址模拟服务:绑定在本机回环地址,用于确认应用是否拒绝访问。
  • 大响应模拟服务:返回超过限制的内容,验证大小控制。
  • 慢响应模拟服务:延迟发送数据,验证超时和连接释放。
  • 多次重定向模拟服务:验证每一跳是否重新校验。

测试用例应覆盖协议、地址、端口和解析行为,而不仅是正常 URL:

def test_reject_non_https(client):
    result = client.validate('
    assert result.is_error


def test_reject_loopback(client):
    result = client.validate('
    assert result.is_error


def test_reject_userinfo(client):
    result = client.validate('
    assert result.is_error


def test_limit_redirects(client):
    result = client.fetch('
    assert result.error_code == 'redirect_limit'


def test_limit_body_size(client):
    result = client.fetch('
    assert result.error_code == 'body_too_large'

在集成测试中,还应检查网络层日志,确认最终连接地址、解析结果和拒绝原因与应用层判断一致。只有应用返回错误并不代表防护有效,还要确认目标服务确实没有收到请求。

八、网络层是最后一道边界

应用层校验存在实现错误的可能,因此不应把安全性完全建立在单一代码判断上。生产环境建议为抓取服务设置独立运行身份和独立网络区域,并采用默认拒绝的出站策略。

  • 仅允许访问业务明确需要的代理或目标网段。
  • 禁止直接访问云实例身份服务、容器管理接口和内部控制面。
  • 限制 DNS 服务器、代理服务器和可用端口。
  • 对出站连接记录目标域名、解析地址、端口、状态码和耗时。
  • 对异常的目标数量、失败率和响应体大小设置告警。
  • 使用最小权限的服务账号,避免抓取进程持有云平台高权限凭据。

网络隔离不能替代应用校验,但可以显著降低单个漏洞的影响范围。尤其是云环境中,实例元数据、短期令牌和服务发现接口都不应依赖应用开发者记得手工拦截,而应由网络策略和身份配置共同保护。

九、日志与事件复盘重点

一次 SSRF 事件复盘,不能只记录“某接口被访问了异常 URL”。建议保留足够的关联信息:

  • 原始输入和规范化后的 URL,但要脱敏其中可能存在的凭据。
  • 最终解析出的 IP、端口、协议和重定向链。
  • 请求来源用户、任务编号、服务实例和执行时间。
  • 连接失败、超时、证书错误和策略拒绝的具体原因。
  • 响应体大小、状态码、Content-Type 和读取耗时。
  • 网络设备是否观察到访问受保护网段的行为。

日志中不要直接记录完整的 Authorization、Cookie 或响应正文。对于疑似攻击请求,可以记录经过哈希或截断处理的标识,并将详细取证数据放在权限受控的审计存储中。

十、常见错误修复方式

错误方式问题改进方向
只拦截几个常见内网地址地址表示、IPv6、DNS 变化和多网卡场景容易遗漏使用标准地址分类,并在连接路径上强制执行校验
只检查域名字符串后缀字符串边界与真实主机名不一致先解析结构,再比较规范化后的主机名
允许自动跟随所有重定向安全的首次地址可能跳转到禁止地址关闭自动重定向,或逐跳校验并限制次数
只限制响应字符数下载、解压和解析阶段仍可能消耗大量资源同时限制字节数、速度、超时和解析资源
完全依赖应用层黑名单代码路径、代理和解析器可能存在绕过增加独立出口代理、网络 ACL 和监控
将用户请求头全部转发可能泄露身份凭据和内部上下文采用固定的最小请求头集合

总结

SSRF 的核心并不是某个特殊字符串,而是“不受信任输入影响了服务端的网络边界”。可靠的防护需要同时处理 URL 规范化、协议和端口限制、DNS 解析、重定向、连接复用、请求头、响应资源和网络出口。

从审计角度看,建议按“输入来源—解析过程—实际连接—响应处理—网络策略—日志告警”的顺序建立检查清单,并在隔离环境中用本地模拟服务验证每条边界。对 URL 预览、截图、远程文件导入、Webhook 测试和链接扫描等功能,都应复用同一套出站访问治理组件,而不是在各个业务模块中分别实现一套不完整的黑名单。

安全边界的目标不是让请求“看起来合法”,而是确保服务端最终建立的每一条连接都符合明确、可审计、默认拒绝的策略。
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

0篇意见

推荐意见

没有意见。

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