SSRF(Server-Side Request Forgery,服务端请求伪造)并不只存在于“允许用户提交 URL”的简单接口中。文件预览、Webhook、图片抓取、富文本转码、PDF 生成以及第三方回调检测等功能,都可能间接产生服务端出网请求。实际审计中,真正困难的部分通常不是发现一个 URL 参数,而是确认请求是否由服务端发起、是否能够触达不应访问的网络区域,以及修复后是否仍存在 DNS 重绑定、重定向或协议解析差异。
一、问题背景与典型场景
以下代码看起来只是一个远程图片下载功能:
public byte[] download(String url) throws IOException {
return httpClient.get(url).getBody();
}如果 url 完全由用户控制,服务端就可能访问内网管理页面、本机服务或云环境中的特殊地址。即使应用本身没有返回响应内容,攻击者仍可能通过状态码、响应时间、错误信息或业务副作用判断目标是否可达。
常见风险来源包括:
- 图片、头像、Markdown 或文档预览服务支持远程地址。
- Webhook 校验功能会主动请求用户填写的回调地址。
- 服务端根据 URL 获取网页标题、生成缩略图或转换 PDF。
- 代理接口允许客户端指定目标地址或请求头。
- 后台任务从数据库读取 URL 并异步访问,导致日志和实时响应中难以定位。
二、审计时先确认“请求是谁发出的”
不要看到参数名为 url 就直接判定为 SSRF。第一步应建立完整的数据流:输入来自哪里,经过哪些解析和校验,最终由哪个 HTTP 客户端发起请求,响应又流向哪里。
在代码审计中,可以重点搜索以下调用点:
- Java:
HttpClient、HttpURLConnection、OkHttp、Apache HttpClient。 - Python:
requests、urllib.request、httpx。 - Node.js:
axios、fetch、got。 - Go:
http.Get、http.Client.Do、自定义反向代理逻辑。
随后向上追踪参数来源,重点检查是否存在字符串拼接、自动跟随重定向、代理环境变量继承、用户可控请求头,以及异步队列中对 URL 的二次处理。
三、不要只校验字符串,要校验解析后的地址
常见错误是使用字符串前缀判断:
if (url.startsWith(" {
request(url);
}这种方式无法可靠处理用户名字段、端口、编码字符、大小写、IPv6 表示法和重定向。例如,业务真正关心的是规范化后的主机、端口和协议,而不是原始字符串是否包含某个前缀。
更稳妥的思路是:
- 只允许业务确实需要的协议,通常仅保留
https,必要时再开放http。 - 使用标准 URL 解析器拆分协议、主机、端口和路径,拒绝解析失败的输入。
- 对主机名做规范化,并解析所有 A、AAAA 记录。
- 拒绝回环地址、链路本地地址、私有地址、组播地址、未指定地址和保留地址。
- 建立连接前再次校验解析结果,避免 DNS 解析与实际连接之间出现不一致。
- 默认禁止跨域重定向;如果业务必须支持,应对每一次重定向重新执行完整校验。
下面是一个用于审计和测试的简化 Python 示例。它不是完整生产实现,但能够体现“解析后校验”的基本方向:
from ipaddress import ip_address
from urllib.parse import urlparse
import socket
ALLOWED_SCHEMES = {"https"}
def is_safe_target(raw_url):
parsed = urlparse(raw_url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
if not parsed.hostname or parsed.username or parsed.password:
return False
if parsed.port not in (None, 443):
return False
try:
records = socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM)
except socket.gaierror:
return False
for item in records:
address = ip_address(item[4][0])
if (address.is_private or address.is_loopback or
address.is_link_local or address.is_multicast or
address.is_unspecified or address.is_reserved):
return False
return True生产环境还需要考虑 DNS 缓存、代理配置、IPv4/IPv6 双栈行为,以及底层 HTTP 客户端是否会重新解析域名。对于高风险场景,仅在应用层做判断并不足够。
四、在隔离环境中验证影响范围
验证 SSRF 不需要访问真实内网,也不应以扫描他人网络为测试手段。可以在本地或专用测试网中启动一个无敏感数据的 HTTP 服务,用于记录请求方法、路径、请求头和来源地址。
python3 -m http.server 18080 --bind 127.0.0.1然后在测试环境中提交指向本地测试服务的地址,观察服务端是否产生访问日志。建议至少记录以下信息:
- 应用节点的源 IP,确认请求是否由服务端发出。
- 请求是否经过代理,以及代理是否修改了目标。
- 是否自动跟随重定向。
- 超时、连接失败和响应过大的处理方式。
- 任务重试机制是否会放大请求次数。
如果服务端不返回目标响应,也可以通过测试服务日志、链路追踪或受控的出站网关日志进行确认。测试数据应使用虚构路径和无敏感内容,避免把验证过程变成对真实资产的探测。
五、容易被忽略的边界条件
1. DNS 重绑定
应用校验域名时得到一个公网地址,但真正建立连接时域名已经解析到内部地址,可能造成校验结果与连接目标不一致。防护重点是让解析、校验和连接使用一致的结果,或在网络出口层实施最终控制。
2. 重定向
即使初始 URL 合法,目标也可能返回指向内网地址的 Location。禁用自动重定向是最简单的策略;若业务必须跟随,应限制跳转次数,并对每个新地址执行协议、端口和 IP 校验。
3. IPv6 与地址表示
只检查 IPv4 私网段会遗漏 IPv6 回环、链路本地和唯一本地地址。解析器、代理和安全组件对 IPv6 的支持也可能不一致,因此应统一使用成熟的 IP 地址库进行判断。
4. 非 HTTP 协议
某些客户端支持 file、ftp、gopher 等协议。若业务只需要网页抓取,应在 URL 解析阶段明确拒绝所有无关协议,而不是依赖底层库的默认行为。
5. 请求体与响应体大小
SSRF 之外,远程抓取还可能引入资源耗尽风险。应限制连接超时、读取超时、响应体大小、最大重定向次数和并发任务数,并对压缩响应进行解压缩后的大小限制。
六、网络层控制比应用层判断更可靠
应用层校验适合减少误用,但不能作为唯一边界。生产环境建议将具备出网能力的服务放入受限网络,默认禁止访问内部管理网段和云平台特殊地址;通过固定的出站代理统一执行域名解析、IP 分类、审计和限流。
同时应检查运行环境中的代理相关环境变量,例如 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。不同语言库对这些变量的默认支持并不完全一致,错误配置可能导致本应经过安全代理的请求直接出网,或绕过既有审计链路。
七、修复后的回归测试清单
- 合法的业务域名能够正常访问。
- 回环、私网、链路本地、组播和未指定地址均被拒绝。
- 域名解析到多个地址时,只要存在禁止地址就按策略拒绝。
- 非允许协议、异常端口、用户名密码形式的 URL 被拒绝。
- 重定向到受限地址时能够阻断。
- 连接超时、读取超时和超大响应能够安全终止。
- DNS 异常、IPv6 地址和解析结果变化不会绕过校验。
- 拒绝原因写入安全日志,但不向外部暴露内部网络细节。
八、总结
SSRF 审计的核心不是寻找一个“可控 URL”,而是判断从输入到网络连接的完整控制链。可靠的处理方式通常包括严格限制协议和端口、对规范化后的地址进行 IP 分类、谨慎处理 DNS 与重定向、限制资源消耗,并在网络出口层建立最终防线。
在漏洞复现时,优先使用本地或专用隔离环境,通过测试服务日志确认服务端请求即可。修复验收则应覆盖解析差异、IPv6、重定向、代理和异步任务等边界条件。只有代码校验、运行时配置和网络策略同时收紧,才能避免“表面修复、实际仍可达”的情况。
推荐意见