SSRF(Server-Side Request Forgery,服务端请求伪造)并不是简单的“让服务器替客户端发一个 HTTP 请求”。在实际项目中,它往往与 URL 解析差异、DNS 解析、重定向、代理配置以及云环境网络边界同时出现,导致漏洞影响被低估。本文以代码审计和授权测试为主线,整理一套适用于业务系统的 SSRF 分析方法,重点放在漏洞成因、低风险验证和工程化修复。
一、典型业务场景与风险边界
常见的 SSRF 入口包括:远程图片抓取、网页缩略图生成、PDF 或文档转换、Webhook 调试、第三方 URL 预览、RSS 导入、代码仓库地址导入,以及根据用户输入探测服务健康状态等功能。
这类功能通常具有一个共同特征:服务端接收用户提供的 URL,随后由后端发起网络请求并处理响应。如果请求目标和协议没有被严格限制,攻击者就可能诱导服务器访问不应暴露的网络资源。
- 访问应用所在网络中的管理接口、测试服务或内部 API。
- 借助服务器身份访问仅对内网开放的资源。
- 通过响应差异推断端口或服务是否存在。
- 在某些环境中读取不应由业务服务访问的配置接口。
需要特别强调的是,SSRF 的影响取决于网络拓扑、服务权限和出站访问策略。不能仅凭存在 URL 请求功能,就直接判定为高危;应结合实际可达范围和数据暴露情况进行分级。
二、代码审计时如何定位 SSRF 入口
审计时不要只搜索 curl 或 requests。更有效的方法是从“用户可控数据如何流向网络请求”入手,建立输入到危险函数的完整数据流。
1. 先找输入源
- HTTP 参数、JSON 字段、表单字段和上传文件中的 URL。
- 数据库中由用户创建或修改的回调地址。
- 消息队列、Webhook、任务调度和导入文件中的远程地址。
- 环境变量或配置中心中可被低权限用户间接修改的地址。
2. 再找网络请求点
- Java 中的
HttpClient、OkHttp、URLConnection。 - Python 中的
requests、urllib、httpx。 - Go 中的
http.Get、http.Client.Do。 - Node.js 中的
fetch、axios、http.request。 - 图片、PDF、Office 转换组件,以及底层会自动解析外部资源的库。
3. 检查中间处理逻辑
很多问题不在请求函数本身,而在请求前后的处理。例如只校验了 URL 的字符串前缀、先校验域名后重新解析、允许自动跟随重定向,或者对异常响应处理不当。审计报告中应记录完整调用链,而不是只标记某一行代码。
三、漏洞成立的关键判断
一个可被利用的 SSRF 通常同时满足以下条件:
- 攻击者能够控制请求目标,或控制目标中的关键部分。
- 服务端确实会发起网络请求,而不是仅在前端展示链接。
- 服务端网络位置与攻击者不同,存在额外的网络可达性。
- 请求结果、状态码、响应时间或错误信息能够被攻击者观察到。
- 目标解析和访问过程中没有可靠的协议、地址和重定向限制。
如果服务端只允许访问固定的可信域名,且在连接前后都进行了地址校验,同时禁用了不必要的重定向,那么即使存在 URL 输入,也不一定构成有效 SSRF。
四、低风险验证方法
在授权测试中,优先使用自建的回显服务或测试域名验证请求是否从服务端发出,不要直接探测生产环境内部系统。验证目标应当是确认请求来源和控制链路,而不是读取敏感数据。
可以在隔离环境中启动一个只记录请求元数据的测试服务:
python3 -m http.server 18080 --bind 127.0.0.1更适合团队协作的方式,是使用内部测试域名,并在 DNS 或网关侧记录请求时间、来源地址、Host 和 User-Agent。测试响应只返回固定文本,不提供目录列表、环境变量或其他敏感信息。
验证时建议按以下顺序进行:
- 使用明确允许的公开测试地址,确认服务端确实发起请求。
- 检查请求是否经过代理,记录实际出口地址和协议。
- 在不访问敏感资源的前提下,测试应用是否允许重定向。
- 测试非法协议、缺少主机名、异常端口等输入能否被稳定拒绝。
- 确认错误信息不会回显内网地址、解析结果、认证头或完整响应内容。
安全验证的目标是证明风险边界,而不是最大化读取内容。对于内部地址、云平台管理接口和生产凭据接口,应使用模拟服务或人工审批后的专用测试端点。
五、最容易被忽略的解析与校验问题
1. 字符串前缀校验不可靠
类似“URL 必须以某个域名开头”的判断容易受到端口、用户信息、大小写、编码和解析差异影响。正确做法是使用标准 URL 解析器分别读取协议、主机、端口和路径,再对结构化字段进行校验。
2. DNS 解析与连接之间存在时间窗口
如果程序先解析域名判断其是否为公网地址,之后又让底层 HTTP 客户端重新解析一次,域名解析结果可能发生变化。防护逻辑应尽量让校验结果与最终连接目标绑定,必要时使用受控 DNS 解析器,并在建立连接时再次确认地址。
3. 自动重定向会绕过首轮校验
首个 URL 可能是允许访问的域名,但响应返回重定向后,客户端又访问了受限地址。应默认关闭自动重定向,或者对每一次跳转重新执行协议、主机、端口和 IP 地址校验,并限制最大跳转次数。
4. IPv4 与 IPv6 规则不一致
只判断常见 IPv4 私网段是不够的。校验逻辑还应覆盖回环地址、链路本地地址、未指定地址、IPv4 映射 IPv6 地址以及组织内部使用的保留网段。建议使用成熟网络地址库,而不是手工编写字符串判断。
5. 代理配置改变了真实出站路径
很多 HTTP 库会自动读取 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 等环境变量。审计时应确认生产环境是否启用了代理,以及代理是否会重新解析目标地址。对于不需要代理的服务,应显式关闭环境代理继承,避免开发环境和生产环境行为不一致。
六、一个较稳妥的防护实现思路
下面的示例只展示防护流程,不包含对真实内部资源的访问。实际项目中还需要结合统一网络出口、审计日志和业务白名单进行设计。
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {"http", "https"}
ALLOWED_PORTS = {80, 443}
def is_public_ip(value):
ip = ipaddress.ip_address(value)
return not (
ip.is_private or
ip.is_loopback or
ip.is_link_local or
ip.is_reserved or
ip.is_multicast or
ip.is_unspecified
)
def validate_target(raw_url):
parsed = urlparse(raw_url)
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
raise ValueError("unsupported scheme")
if not parsed.hostname:
raise ValueError("missing hostname")
if parsed.username or parsed.password:
raise ValueError("userinfo is not allowed")
port = parsed.port or (443 if parsed.scheme.lower() == "https" else 80)
if port not in ALLOWED_PORTS:
raise ValueError("unsupported port")
addresses = socket.getaddrinfo(
parsed.hostname, port, type=socket.SOCK_STREAM
)
resolved = {item[4][0] for item in addresses}
if not resolved or not all(is_public_ip(item) for item in resolved):
raise ValueError("target address is not allowed")
return parsed, resolved这个示例仍然不能直接作为通用生产组件使用,原因包括 DNS 重新解析、连接复用、代理、重定向和域名白名单策略等。更推荐将访问能力集中封装成一个内部服务,统一实现超时、大小限制、响应类型限制和审计,而不是让业务代码到处直接调用 HTTP 客户端。
七、工程化防护清单
| 控制项 | 建议 |
|---|---|
| 协议 | 只允许业务真正需要的 HTTP 或 HTTPS,拒绝文件、脚本、字典和其他非业务协议。 |
| 主机 | 优先使用严格域名白名单;无法白名单时,至少拒绝本地、私有、保留和链路本地地址。 |
| 端口 | 限制到明确业务端口,禁止任意端口访问。 |
| DNS | 统一解析策略,避免校验解析和连接解析不一致,并关注 DNS 变更。 |
| 重定向 | 默认关闭;确需开启时逐跳校验并限制跳转次数。 |
| 请求头 | 不要把用户的 Cookie、Authorization 或内部追踪头转发到目标站点。 |
| 响应处理 | 限制响应体大小、连接时间和总耗时,按需限制 Content-Type,避免直接回显完整响应。 |
| 网络层 | 在出口防火墙、云安全组和代理层再次限制出站范围,形成纵深防御。 |
| 日志 | 记录业务用户、目标域名、解析地址、状态码、耗时和拒绝原因,但避免记录敏感响应内容。 |
八、代码审计报告应该写清什么
一份可复现、可修复的 SSRF 报告至少应包含以下内容:
- 漏洞入口及其权限要求。
- 用户输入到网络请求函数的完整调用链。
- 当前支持的协议、端口、重定向和代理行为。
- 在隔离环境中观察到的服务端请求证据。
- 实际影响范围,例如只能访问公网,还是可以触达受限网络。
- 不涉及敏感数据的复现步骤和测试地址。
- 短期修复措施与长期架构改进建议。
如果无法确认内部网络可达性,应如实标记为“潜在 SSRF”或“服务端请求行为待确认”,不要在报告中夸大影响。反过来,如果已经确认请求会经过高权限代理或能够访问内部管理面,也应将网络拓扑和权限边界写清楚。
九、修复后的回归测试
修复不能只验证一个被拒绝的 URL。建议建立覆盖解析差异的自动化测试集,至少包括:
- 合法的 HTTP 和 HTTPS 地址。
- 缺少协议、缺少主机名和非法端口。
- 用户名密码形式的 URL。
- IPv4、IPv6、回环、私网、链路本地和保留地址。
- 解析到多个地址且其中包含受限地址的域名。
- 返回重定向到受限目标的测试服务。
- 超时、超大响应、错误 Content-Type 和连接异常。
- 代理环境变量存在与不存在时的行为差异。
同时应在网络层保留一份独立的拒绝策略。应用层校验出现遗漏时,出口防火墙仍然能够阻止服务访问不应到达的网段。
总结
SSRF 审计的核心不是寻找某个特殊字符串,而是理解“输入解析、DNS 解析、建立连接、重定向和响应处理”这一整条链路。可靠的防护通常由四部分组成:严格限制协议和目标、避免解析与连接之间的差异、关闭或重新校验重定向,以及在网络出口实施独立的访问控制。
在实际项目中,优先采用域名白名单、受控出站代理和统一请求组件,配合低风险的回显服务完成验证。这样既能提高漏洞判断的准确性,也能避免测试过程对生产系统和内部敏感资源造成额外影响。
推荐意见