服务端请求伪造(SSRF)并不只是一个“能否访问内网”的简单问题。实际审计中,风险通常分散在 URL 解析、DNS 解析、重定向、代理配置和网络出口策略等多个环节。单纯增加一个正则表达式,往往只能拦住最直观的测试用例,却无法覆盖 IPv6、DNS 重绑定、编码差异以及跳转链等场景。
本文以一个常见的“远程图片抓取”功能为例,整理从代码定位、风险确认、修复设计到上线验证的完整思路。示例仅用于本地测试环境和防御性审计,不涉及访问真实内网服务或敏感信息。
一、问题背景与典型场景
很多业务会提供类似功能:用户提交一个图片或文档 URL,后端下载资源后进行转码、缓存或内容审核。伪代码通常如下:
def fetch_resource(url):
response = http_client.get(url, timeout=10)
return response.content如果这个 URL 完全由用户控制,服务端就可能成为一个网络请求代理。风险主要包括:
- 访问本不应由公网用户触达的管理接口、监控接口或内部服务。
- 将内网服务的响应内容带回前端,造成信息泄露。
- 借助重定向、代理或特殊地址表示绕过简单的域名黑名单。
- 消耗服务端连接、带宽和线程资源,形成应用层资源滥用。
需要注意的是,SSRF 是否构成高危,取决于网络分区、服务权限、响应处理方式和出口策略。一个只能访问固定公网域名的抓取服务,与一个可以访问整个内部网络的服务,实际影响完全不同。
二、代码审计时的定位方法
审计时不要只搜索“requests.get”或“HttpClient”。更有效的方法是从用户可控输入开始,沿数据流追踪到网络请求函数。
- 查找输入来源:HTTP 参数、JSON 字段、文件内容、Webhook 地址、Markdown 链接以及任务配置。
- 确认是否经过拼接或二次编码,例如 URL 被放入代理参数、重定向参数或 HTML 解析流程。
- 追踪最终请求函数,记录是否允许自动跳转、是否使用系统代理、是否验证 TLS、是否设置超时。
- 检查响应是否会被原样返回、写入日志、保存到对象存储,或交给后续解析器处理。
- 确认运行环境的网络权限,包括容器网段、云平台元数据访问、服务账号权限和出站防火墙策略。
在审计记录中,建议同时保留“输入点—校验点—解析点—请求点—输出点”五个位置。很多漏洞并非没有校验,而是校验发生在错误的解析阶段,或者校验后的 URL 在后续流程中被重新解析。
三、URL 校验中最容易出错的地方
1. 只做字符串匹配
类似 url.startswith(" 的判断不等价于域名校验。字符串前缀可能匹配到相似域名、用户名字段或路径内容。正确做法是使用标准 URL 解析器,分别检查 scheme、hostname、port 和 path。
2. 只检查域名,不检查解析结果
域名最终会解析为 IP 地址。即使域名本身看起来可信,也需要确认解析结果是否落入私网、回环、链路本地、保留地址或其他不允许的网络范围。IPv4 映射 IPv6 地址、十进制或十六进制地址表示,也不能依赖字符串规则判断。
3. 忽略重定向
初始地址可能是允许访问的公网地址,但服务端跟随 30x 跳转后,目标已经发生变化。因此需要关闭自动重定向,或者对每一次跳转重新执行完整校验,并限制最大跳转次数。
4. 校验与连接之间存在时间差
先 DNS 解析、再发起请求的流程存在 DNS 重绑定风险。一次解析得到公网 IP,并不意味着连接建立时仍然使用同一个地址。对高风险场景,应用层校验应配合固定出口代理、网络 ACL 和连接级地址控制,不能只依赖业务代码。
四、一个防御性校验示例
下面的代码展示了基础校验思路,适合作为审计或单元测试的起点。它不是完整的网络隔离方案,生产环境仍应通过受控代理和出站防火墙建立最后一道边界。
from urllib.parse import urlsplit
import ipaddress
import socket
ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}
def host_resolves_to_public_ip(host):
try:
records = socket.getaddrinfo(
host, None, type=socket.SOCK_STREAM
)
except socket.gaierror:
return False
if not records:
return False
for record in records:
address = record[4][0]
ip = ipaddress.ip_address(address)
if not ip.is_global:
return False
return True
def validate_target(raw_url):
parsed = urlsplit(raw_url)
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
return False
if not parsed.hostname:
return False
if parsed.username or parsed.password:
return False
try:
port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
except ValueError:
return False
if port not in ALLOWED_PORTS:
return False
if not host_resolves_to_public_ip(parsed.hostname):
return False
return True这个示例包含几个关键点:使用结构化解析而不是字符串拼接;拒绝用户名和密码字段;限制协议与端口;将域名解析为 IP 后检查地址属性;对解析失败和异常端口直接拒绝。
但需要特别强调,getaddrinfo 检查与后续 HTTP 连接之间仍然可能存在时间窗口。更稳妥的架构是让应用只向一个受控抓取代理发请求,由代理统一完成 DNS 解析、地址过滤、连接建立和重定向检查,业务服务本身不直接访问任意目标。
五、请求层的安全控制
URL 校验之外,请求客户端还应设置明确的资源边界:
- 禁止或严格限制自动重定向,限制最大跳转次数。
- 设置连接超时、读取超时、总耗时和最大响应体大小。
- 限制响应内容类型,只允许业务真正需要的类型,例如图片或纯文本。
- 禁止将用户输入直接作为代理地址、请求头或认证信息。
- 对压缩响应、分块传输和异常 Content-Length 做大小保护,避免资源耗尽。
- 下载完成后重新检测文件类型,不要仅依据扩展名或响应头。
以 Python 客户端为例,基础配置可以类似这样:
response = session.get(
target_url,
allow_redirects=False,
timeout=(3, 8),
stream=True,
headers={"Accept": "image/*"}
)
content_length = response.headers.get("Content-Length")
if content_length and int(content_length) > 5 * 1024 * 1024:
raise ValueError("response too large")实际读取响应体时,还需要在流式读取过程中累计字节数,不能只依赖 Content-Length,因为服务端可能不返回该字段,或者返回不可信的值。
六、网络层的纵深防御
代码修复解决的是应用层逻辑,网络层仍然需要独立控制:
- 抓取服务使用独立网络区域,默认拒绝访问数据库、缓存、消息队列和管理网段。
- 通过出站防火墙或 egress proxy 只允许访问必要的公网端口和域名。
- 明确禁止访问回环、链路本地、私网、保留地址以及云环境敏感管理地址。
- 为抓取服务使用最小权限的服务账号,避免网络访问能力与云 API 权限叠加。
- 记录目标域名、解析结果、最终连接地址、响应状态、响应大小和耗时,便于追溯。
日志中不要保存完整的认证 URL、Cookie 或可能包含敏感参数的请求头。对于用户提交的 URL,可以记录脱敏后的规范化结果和哈希值,既方便关联分析,也能减少二次泄露风险。
七、如何在测试环境中验证修复
验证应优先使用自建的多个测试服务,不要把测试请求指向生产内网或第三方基础设施。可以准备以下场景:
- 正常公网 HTTPS 地址,确认业务功能不受影响。
- 回环地址、私网地址、链路本地地址和 IPv4 映射 IPv6 地址,确认被拒绝。
- 包含用户名字段、非标准端口和异常编码的 URL,确认解析行为一致。
- 返回 30x 的测试服务,确认跳转目标会重新校验。
- 返回超大响应、慢速响应和错误 Content-Type 的测试服务,确认超时与大小限制生效。
- DNS 解析失败、返回多个地址或地址变化时,确认默认采取拒绝策略。
建议把这些场景固化为单元测试和集成测试,尤其关注不同 URL 解析库之间的行为差异。升级运行时、HTTP 客户端或代理组件后,应重新执行这组测试。
八、常见修复误区
- 只维护黑名单:黑名单无法覆盖所有地址表示和未来新增网段,优先采用明确的协议、端口和目标范围白名单。
- 只校验 hostname:必须结合 DNS 结果、重定向目标和实际连接地址判断。
- 认为 HTTPS 可以解决 SSRF:HTTPS 只保护传输和证书校验,并不会阻止请求到达错误的网络位置。
- 把解析逻辑散落在多个业务模块:应集中封装抓取能力,统一超时、大小、跳转和审计策略。
- 只看 HTTP 状态码:响应体大小、内容类型、耗时和解析器行为同样可能造成安全问题。
九、总结
SSRF 审计的重点不是寻找一个“万能正则”,而是完整理解从用户输入到网络连接的整个链路。可靠的防护通常由四部分组成:严格的 URL 结构校验、对解析结果和跳转目标的持续检查、受控的请求客户端配置,以及网络出口层的默认拒绝策略。
如果业务确实只需要抓取少量固定站点,最安全的方案通常不是允许任意 URL 后不断补丁,而是改成站点白名单或资源标识映射。对于必须支持动态目标的场景,也应优先采用隔离的抓取服务和受控代理,并通过日志与测试用例持续验证边界。这样才能把一次代码缺陷,降低为可监控、可限制、可回滚的工程风险。
推荐意见