SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可控 URL”这么简单。真正有价值的分析,需要回答三个问题:请求是否真的由服务端发起、请求能否到达不应暴露的网络区域、攻击者能否读取或影响响应结果。本文以代码审计和安全验证为主线,整理一套适用于 Web 服务、Webhook、图片抓取和文档解析功能的 SSRF 分析方法。
一、典型场景与风险边界
常见的 SSRF 入口包括远程图片下载、URL 预览、Webhook 回调、PDF 或网页转码、第三方接口连通性检测等。开发者通常会接收一个 URL,然后由后端使用 HTTP 客户端访问该地址,最后将状态码、响应体或部分响应信息返回给用户。
风险并不只来自访问内网地址。即使目标服务无法直接读取响应,仍可能通过请求时间、状态码、错误信息或日志造成信息泄露。因此,审计时应分别评估网络可达性、响应可见性和请求副作用。
| 风险维度 | 需要关注的问题 |
|---|---|
| 网络范围 | 是否可以访问本机、容器网段、办公网或管理网段 |
| 协议能力 | 是否仅允许 HTTP/HTTPS,是否错误地支持其他协议 |
| 响应处理 | 是否把完整响应、错误信息或响应头返回给用户 |
| 请求副作用 | 是否会触发内部服务的写操作、任务执行或状态变更 |
| 解析一致性 | 域名解析、重定向和连接时使用的地址是否经过同一套校验 |
二、代码审计:从输入源追踪到网络出口
审计时不要只搜索“url”或“request”关键字,更有效的方法是先找外部输入,再沿数据流追踪到网络请求函数。常见入口包括 JSON 字段、表单参数、HTTP Header、数据库中的回调地址以及消息队列中的任务参数。
以下是一个具有代表性的危险写法:
def fetch_preview(user_url):
response = requests.get(user_url, timeout=10)
return response.text这段代码的问题不只在于缺少域名限制,还包括没有限制协议、没有限制响应大小、默认跟随重定向,并且直接把完整响应返回给调用方。审计报告中应明确指出每个问题对应的影响,而不是笼统地写成“存在 SSRF”。
进一步分析时,可以建立如下调用链:
HTTP 参数 url
-> URL 规范化
-> 域名解析
-> IP 访问控制
-> HTTP 客户端连接
-> 重定向处理
-> 响应读取与返回如果校验发生在错误的位置,例如只检查初始 URL 字符串,却没有检查重定向后的目标,或者校验时解析一次地址、连接时又重新解析一次地址,就可能形成校验与实际请求不一致的问题。
三、安全验证:使用本地实验环境完成闭环
验证 SSRF 时,建议使用本地构造的两个服务:一个作为业务服务,接收用户提供的 URL;另一个作为模拟内部服务,仅监听本机或测试网段。这样既能确认请求是否由服务端发起,也不会触碰真实生产系统。
模拟内部服务可以只返回固定内容:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"internal-test-service"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()验证时重点记录以下证据:
- 业务服务的访问日志中是否出现测试请求。
- 模拟内部服务是否收到连接,以及请求路径和请求头。
- 业务接口返回的是完整响应、状态码、错误信息还是超时。
- 请求是否经过代理、DNS 服务或其他中间组件。
- 重定向、超时和大响应场景下,服务是否仍然稳定。
不要把“本机地址可访问”直接等同于高危。风险等级还取决于服务进程权限、网络分段、响应可见性以及目标接口是否具备写操作。报告应将可验证事实与推测性影响分开描述。
四、URL 校验中最容易遗漏的细节
SSRF 防护的难点在于 URL 是一个结构化对象,不能只用字符串前缀判断。例如,检查 URL 是否以某个域名开头,无法可靠区分真正的主机名和只是出现在用户名、路径或查询参数中的字符串。
建议至少执行以下步骤:
- 只允许业务确实需要的协议,通常为 HTTP 和 HTTPS。
- 解析 URL 后单独获取主机名、端口和路径,不使用字符串包含判断。
- 拒绝用户名、密码和不必要的非标准端口。
- 对主机名进行规范化,处理大小写、末尾点和国际化域名等情况。
- 解析所有 A 和 AAAA 记录,并对每个结果执行地址范围检查。
- 明确拒绝回环地址、链路本地地址、私有地址、组播地址、未指定地址及企业内部保留网段。
- 禁用或严格控制自动重定向,并对每一次重定向重新执行完整校验。
- 限制响应体大小、连接时间、读取时间和总请求时间。
IPv4 和 IPv6 必须同时考虑。很多实现只判断常见的 IPv4 私网段,却遗漏 IPv6 回环地址或链路本地地址。还要注意 DNS 解析结果可能有多个地址,不能只检查第一个结果。
五、一个更稳妥的防护实现思路
下面示例展示防护逻辑的结构,重点是分层校验,不代表可以直接替代经过充分测试的生产级 HTTP 客户端。
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {"http", "https"}
class UnsafeTarget(Exception):
pass
def validate_target(raw_url):
parsed = urlparse(raw_url)
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
raise UnsafeTarget("unsupported scheme")
if not parsed.hostname or parsed.username or parsed.password:
raise UnsafeTarget("invalid host information")
if parsed.port not in (None, 80, 443):
raise UnsafeTarget("unsupported port")
host = parsed.hostname.rstrip(".").lower()
addresses = socket.getaddrinfo(host, parsed.port or 443, type=socket.SOCK_STREAM)
if not addresses:
raise UnsafeTarget("no address")
for item in addresses:
ip = ipaddress.ip_address(item[4][0])
if (ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_multicast or ip.is_unspecified):
raise UnsafeTarget("blocked address")
return parsed实际实现还应避免解析与连接之间的地址漂移问题。较稳妥的方式是由受控的网络层或代理统一执行出口策略,并在连接阶段绑定经过校验的地址;如果使用域名直接交给底层客户端,应确认底层解析、连接和重定向行为符合预期。
六、重定向、DNS 与代理带来的差异
重定向是审计中经常被忽略的路径。初始地址可能属于允许访问的站点,但响应中的 Location 可能指向另一个网络区域。因此,禁用自动重定向通常是更容易审计的选择;如果业务必须支持重定向,则每一跳都应重新解析、校验并限制最大跳数。
DNS 相关问题也不能只靠一次解析解决。域名可能返回多个地址,解析结果可能变化,代理还可能在另一侧重新解析域名。防护策略应明确请求是由应用直连还是由出站代理代发,并在网络层设置默认拒绝的出口规则,避免仅依赖应用代码。
代码校验解决“应用认为目标是否可信”,网络出口策略解决“即使代码出现遗漏,服务实际上还能访问什么”。两者应同时存在。
七、响应处理和资源消耗
即使目标地址校验正确,远程抓取功能仍可能被滥用于资源消耗。常见问题包括无限读取响应、压缩包或转码任务过大、连接池耗尽,以及对慢速响应长期等待。
- 设置连接超时、读取超时和总超时,不要只设置一个超时参数。
- 使用流式读取,并限制最大响应字节数。
- 限制并发任务数量,对同一用户或同一目标做速率控制。
- 禁止自动解压不可信内容,或对解压后的文件数量和总大小设上限。
- 不要把远端响应头原样转发给浏览器,避免引入缓存、跳转和内容类型问题。
- 日志中记录目标域名、解析结果、最终状态、耗时和拒绝原因,但避免记录敏感响应正文。
八、审计报告应该怎样落地
一份可执行的 SSRF 报告至少应包含:受影响接口、输入参数、调用链、验证环境、请求是否成功到达测试服务、响应是否可见、可访问的网络范围、影响分析和修复建议。
修复建议不要只写“增加 URL 白名单”。应根据业务模式给出具体方案:固定第三方服务时使用精确域名白名单;只需要回调时采用预注册的目标标识而不是直接接受完整 URL;必须抓取任意公网资源时,使用独立出站代理、默认拒绝的网络策略和严格的资源限制。
九、总结
SSRF 分析的核心不是寻找某个神奇的绕过写法,而是建立从输入、解析、DNS、连接、重定向到响应处理的完整链路。审计时应同时关注协议、地址范围、解析一致性、网络出口和资源消耗;验证时使用本地模拟服务保留可复现证据;修复时采用应用层校验与网络层隔离的纵深防御。
对于长期维护的系统,建议将 SSRF 防护纳入代码评审清单和回归测试,并对 HTTP 客户端、代理配置及依赖库升级进行专项复核。这样才能避免一次修复后,因新增抓取功能或更换网络组件再次引入同类问题。
推荐意见