SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替用户发一个 HTTP 请求”这么简单。真正需要关注的是:用户可控数据是否进入了服务端请求链路,服务端请求具有什么网络权限,响应内容是否会回显,以及应用是否错误地把网络位置当成了可信身份。
在实际审计中,SSRF 往往隐藏在图片抓取、Webhook 调试、在线导入、PDF 生成、链接预览、RSS 订阅和云资源配置等功能中。单独看某个请求参数,风险可能并不明显;但一旦结合内网可达性、云平台元数据接口、管理面板或未授权服务,就可能形成严重的数据泄露和横向影响。
一、先明确需要审计的问题
审计 SSRF 时,不要只搜索 request、get 或 curl 等关键词。更有效的方式是沿着“输入—解析—请求—响应—输出”五个环节建立数据流:
- 输入:URL 是否来自请求参数、JSON 字段、文件内容、数据库记录或消息队列?
- 解析:应用是否对协议、主机名、端口和重定向进行过校验?
- 请求:使用了什么 HTTP 客户端,是否自动跟随重定向,是否支持 file、gopher 等非 HTTP 协议?
- 响应:响应体、状态码、响应头或错误信息是否返回给用户?
- 输出:请求结果是否被写入日志、缓存、文件、消息通知或后续任务?
一个常见的误区是只检查目标 URL 的初始字符串。例如,代码可能拒绝了 127.0.0.1,却没有考虑域名解析后指向内网地址、IPv6 回环地址、十进制 IPv4 表示法,或者通过重定向跳转到受限网络。
二、一个典型的漏洞场景
下面的示例是一个仅用于本地实验的链接预览接口。它接收用户提交的 URL,服务端抓取页面标题并返回结果:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
@app.post('/preview')
def preview():
target = request.form.get('url', '')
response = requests.get(target, timeout=5)
return jsonify({
'status': response.status_code,
'title_source': response.text[:2000]
})问题不在于 requests.get 本身,而在于 target 完全由外部输入控制。服务端可能访问本机服务、容器网络、企业内网或云环境中的内部接口。如果响应内容被完整返回,风险会进一步扩大;即使响应不回显,仍可能通过请求时间、状态码、错误信息或日志观察到内部服务是否存在。
在代码审计中,还应继续向上追踪 target 的来源。例如 URL 可能先进入数据库,再由定时任务异步处理;也可能由管理员配置,普通用户无法直接调用,但低权限用户能够影响配置内容。异步任务并不会天然降低风险,只是改变了触发方式。
三、在本地环境完成可控复现
复现时建议搭建两个本地服务:一个模拟业务服务,另一个模拟内部服务。所有请求都限制在本机或专用测试网络,不要把测试目标指向互联网上的真实地址。
from flask import Flask
app = Flask(__name__)
@app.get('/internal-check')
def internal_check():
return 'local test service only'
app.run(host='127.0.0.1', port=9001)启动内部测试服务后,再调用业务接口:
curl -X POST \
-d 'url= local test service only,说明用户输入确实穿透到了服务端请求层。这个验证已经足以证明存在 SSRF 数据流,不需要继续访问任何真实内网系统。为了验证重定向处理是否存在缺陷,可以在本地再启动一个测试服务,让它返回 302 并指向另一个本地端口。重点观察 HTTP 客户端是否自动跟随,以及应用是在每次跳转前重新执行地址校验,还是只检查初始 URL。
四、审计中最容易漏掉的边界问题
1. DNS 解析与 IP 校验不一致
如果应用先校验域名字符串,再由 HTTP 客户端解析域名,校验对象和实际连接对象可能不是同一个。域名解析结果可能包含公网地址和内网地址,也可能随着时间变化。更稳妥的做法是:
- 解析域名得到所有 A 和 AAAA 记录。
- 逐个判断是否属于回环、链路本地、私有、保留或其他禁止网段。
- 连接时固定经过校验的解析结果,避免校验和连接之间再次发生不受控解析。
- 在网络层配置出口策略,不能只依赖应用层判断。
需要特别注意 IPv6。只过滤 127.0.0.0/8 并不能覆盖 ::1、链路本地地址以及其他本地或保留范围。
2. 重定向绕过初始校验
服务端可能先检查一个公网 URL,但该 URL 返回重定向后,客户端自动访问了内网地址。建议默认关闭自动重定向;如果业务确实需要,应限制跳转次数,并对每个 Location 重新执行协议、主机、端口和 IP 校验。
3. URL 解析器差异
不同语言、库和代理对 URL 的解析细节可能不同。用户名信息、端口、编码字符、大小写、尾随点、IPv6 方括号和非标准 IP 表示法,都可能造成“校验结果”和“实际连接目标”不一致。不要通过字符串前缀、简单正则或黑名单处理复杂 URL。
4. 协议和端口范围
业务通常只需要 HTTPS 或 HTTP,就应采用明确的协议白名单,并限制端口范围。不要把“只禁止 file”当成完整防护,也不要默认所有 HTTP 客户端行为都相同。若底层库支持其他协议,应在适配层显式关闭或隔离。
五、一个更安全的校验思路
下面示例展示的是防御思路,不是可以直接覆盖所有业务的通用组件。生产环境应结合语言运行时、HTTP 客户端和网络架构进行测试:
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {'http', 'https'}
ALLOWED_PORTS = {80, 443}
def is_public_ip(value):
address = ipaddress.ip_address(value)
return not (
address.is_private or
address.is_loopback or
address.is_link_local or
address.is_reserved or
address.is_multicast or
address.is_unspecified
)
def validate_target(raw_url):
parsed = urlparse(raw_url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError('scheme is not allowed')
if not parsed.hostname:
raise ValueError('hostname is required')
if parsed.username or parsed.password:
raise ValueError('userinfo is not allowed')
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
if port not in ALLOWED_PORTS:
raise ValueError('port is not allowed')
addresses = socket.getaddrinfo(
parsed.hostname,
port,
type=socket.SOCK_STREAM
)
for item in addresses:
ip_value = item[4][0]
if not is_public_ip(ip_value):
raise ValueError('resolved address is not allowed')
return parsed
这段代码仍然不能单独解决 DNS 重绑定问题,因为“解析”和“建立连接”之间可能存在时间窗口。更可靠的实现通常包括自定义解析器、将解析结果传递给连接层、禁止自动跳转,并在出口防火墙或代理层再次限制目标网络。
六、修复不能只停留在代码层
SSRF 的根本风险是应用服务器拥有过大的出网能力。因此建议采用分层防护:
- 业务层:对 URL 使用协议、域名、端口和解析结果白名单,默认关闭重定向。
- 客户端层:设置连接超时、读取超时、响应体大小上限和最大跳转次数,避免请求被用于资源消耗。
- 网络层:应用容器默认禁止访问管理网段、云元数据地址、数据库网段和内部控制面;需要访问的服务使用明确的出口代理。
- 身份层:不要把高权限凭据放在应用可以随意访问的位置;云环境应使用最小权限身份,并采用平台提供的元数据访问保护机制。
- 日志层:记录请求任务、目标域名、解析地址、最终连接地址、跳转链和拒绝原因,但注意脱敏 URL 中可能出现的凭据。
对于图片抓取、文档转换等高风险功能,最好将网络访问放入独立沙箱。沙箱不应与主业务共享网络命名空间、云角色、敏感文件和长期凭据。
七、如何判断修复是否有效
修复验证不能只测试一个 127.0.0.1。建议在本地测试环境建立覆盖矩阵:
测试类别 预期结果 允许的 HTTPS 公网测试域名 请求成功,并受超时和大小限制 HTTP 或 HTTPS 以外的协议 在请求发出前拒绝 本地回环地址和 IPv6 回环地址 拒绝 私有地址、链路本地地址和保留地址 拒绝 解析到受限地址的测试域名 拒绝 跳转到受限地址的本地测试服务 跳转前或跳转后拒绝 超大响应、慢响应和过多跳转 按策略终止,不影响工作线程
此外,还应通过单元测试覆盖 URL 解析、域名解析失败、IPv4 和 IPv6、显式端口、默认端口以及异常响应。若系统使用代理,必须分别测试“直连”和“代理请求”两条路径,因为代理可能重新解析域名,导致应用层的校验假设失效。
八、事件复盘中值得关注的信号
如果怀疑某个链接抓取功能被滥用,可以从访问日志中寻找异常模式:短时间内大量不同主机名、访问内网保留地址、频繁出现连接超时、非正常端口、跳转链异常增长,以及请求目标与正常业务明显不符。日志中的目标地址不应直接等同于最终连接地址,最好同时记录解析结果和网络出口。
复盘时还要检查是否存在二次影响,例如响应内容是否进入缓存、是否被写入构建产物、是否通过通知系统发送给其他人员,以及抓取任务是否使用了高权限云身份。很多 SSRF 事件的影响并不发生在首次请求,而是发生在响应被后续组件信任之后。
总结
SSRF 审计的关键不是寻找某个“危险 URL”,而是识别应用是否把外部输入转化成了受服务端网络权限保护的请求。可靠的防护应同时覆盖数据流、URL 解析、DNS 解析、重定向、协议限制、网络出口和身份权限。
复现时使用本地服务即可证明漏洞链路;修复时则要验证各种地址表示、解析结果和跳转行为,并把最终控制落到网络隔离和最小权限上。只有代码校验与基础设施策略同时生效,才不会因为一个解析器差异或一次重定向,让原本看似完善的防护失效。

服务器、云资产与权限边界梳理示意
推荐意见