服务端请求伪造(SSRF)并不是“让服务器代替用户访问一个 URL”这么简单。真正危险的地方在于:应用服务器通常处于比普通用户更可信的网络位置,能够访问本地管理接口、云平台元数据服务、内网 API 或仅绑定在回环地址上的调试端口。一旦 URL 参数、Webhook、图片抓取、文档转换等功能缺少边界控制,原本的业务能力就可能变成跨网络边界的请求代理。
本文从代码审计和防守视角,整理一套适用于 Java、Python 等常见后端的 SSRF 分析方法。示例均以本地测试环境和无敏感数据的服务为前提,重点放在漏洞判断、根因定位和修复验证。
一、哪些功能容易引入 SSRF
审计时不要只搜索名为 url、uri、link 的参数,更应关注“服务端根据外部输入发起网络请求”的行为。常见入口包括:
- 远程图片下载、头像抓取、网页预览。
- Webhook、回调地址、第三方通知地址。
- 文件导入、在线 XML 或 Office 文档解析。
- URL 转 PDF、截图、网页内容转 Markdown。
- 代理接口、健康检查、连通性测试功能。
- 根据用户输入访问 Git、对象存储或内部配置中心。
代码层面可以优先检索 HTTP 客户端调用、URL 构造和重定向配置,例如 Java 中的 HttpClient、OkHttp、URLConnection,Python 中的 requests、urllib、httpx,Node.js 中的 axios、fetch、got 等。搜索结果不能直接等同于漏洞,关键是确认请求目标是否受外部输入影响,以及请求进程所在网络区域能访问什么资源。
二、先判断是“可控请求”还是“真正的 SSRF”
审计时建议把判断拆成三个问题:
- 输入是否可控:攻击者能否直接提交 URL,或通过 JSON、表单、HTTP 头、文件内容间接影响目标地址。
- 请求是否由服务端发起:浏览器端跳转、前端 fetch 与服务端 HTTP 客户端的风险边界不同。
- 服务端是否能访问高价值网络:包括本机回环地址、容器网段、云元数据地址、办公网或管理网段。
如果服务端只允许访问固定的第三方域名,且 DNS、重定向和端口均受到约束,风险通常可控。如果服务端接受任意绝对 URL,即使响应内容不会返回给用户,也可能形成盲 SSRF:请求本身可以触发内部服务日志、状态变更、出网探测或带外回连。
一个实用的审计原则是:不要只问“能不能读到响应”,还要问“这个请求是否能产生副作用”。盲 SSRF 同样需要按高风险路径处理。
三、典型漏洞根因:只校验字符串,没有校验最终连接目标
下面是常见但不安全的逻辑示意:
String target = request.getParameter("url");
HttpGet httpGet = new HttpGet(target);
try (CloseableHttpResponse response = client.execute(httpGet)) {
return EntityUtils.toString(response.getEntity());
}这段代码的问题不只是“没有白名单”。即使增加了简单的字符串判断,也可能遗漏 URL 解析、DNS 解析、重定向和 IP 表示方式带来的边界问题。
四、审计时需要重点检查的绕过面
1. URL 解析差异
不同语言和库对用户名密码、端口、编码字符、反斜杠以及异常 URL 的解析规则并不完全一致。业务层如果使用一个解析器判断域名,HTTP 客户端又使用另一个解析器建立连接,就可能出现“校验结果”和“实际连接目标”不一致。
建议统一使用成熟 URL 解析库,不要通过 startsWith、contains 或字符串拼接判断域名。例如,以下逻辑并不能证明目标属于 example.com:
if (url.contains("example.com")) {
// 错误示例:无法证明最终主机就是允许域名
}2. DNS 解析与 DNS Rebinding
域名校验后再进行连接,存在时间窗口问题。域名第一次解析到公网地址,连接时可能解析到内网地址;也可能解析结果中同时包含公网和私网地址,而客户端最终选择了未被检查的地址。
防守时应在解析阶段取得全部地址,逐个判断是否属于禁止范围,并尽量让后续连接复用已经校验过的解析结果,而不是把原始域名交给客户端再次解析。对于高风险场景,可使用受控 DNS 解析器和固定出口代理,从网络层进一步限制访问范围。
3. IPv4、IPv6 和特殊地址表示
只判断常见 IPv4 私网段是不够的,还应覆盖:
- 回环地址、链路本地地址、未指定地址。
- RFC1918 私网、运营商级 NAT 地址和文档规定的保留网段。
- IPv6 回环、链路本地、唯一本地地址。
- IPv4-mapped IPv6 地址。
- 十进制、十六进制或其他库接受的非标准 IPv4 表示。
实际实现中,应优先使用标准 IP 地址解析库,将地址规范化后再判断,而不是自行编写正则表达式。
4. HTTP 重定向
即使初始 URL 通过检查,目标服务器也可能返回 3xx,将客户端引导到新的主机。若 HTTP 客户端默认跟随重定向,最终连接目标可能完全绕过初始校验。
稳妥做法是默认关闭自动重定向;如果业务确实需要跟随,则每一次重定向都重新执行协议、端口、域名和 IP 检查,并限制最大跳转次数。
5. 协议和端口限制
业务只需要下载 HTTPS 图片时,就不要接受 file、ftp、gopher 或其他协议。端口也应采用明确白名单,例如只允许 443,必要时再允许 80。禁止将用户输入直接交给支持多协议的通用客户端。
五、一个可落地的安全请求流程
对于“服务端抓取远程资源”这类功能,可以按以下顺序处理:
- 限制 URL 长度,拒绝控制字符和不必要的编码形式。
- 只允许明确的协议,例如 HTTPS。
- 解析主机名、端口和路径,不接受缺省逻辑不明确的输入。
- 如果业务采用域名白名单,使用规范化后的主机名进行精确匹配,而不是后缀模糊匹配。
- 解析全部 IP,并拒绝回环、链路本地、私网、保留和未指定地址。
- 关闭自动重定向,或对每一跳重新执行完整校验。
- 设置连接超时、读取超时、响应体大小和总下载时长。
- 下载完成后按业务需要校验内容类型,不要仅信任 Content-Type。
- 将请求放在独立网络区域,使用出口防火墙限制可访问的目的网段。
六、Java 实现中的关键控制点
下面示例展示控制思路,生产环境应结合项目使用的 HTTP 客户端和网络架构完善异常处理、日志脱敏及连接池配置:
URI uri = URI.create(userInput);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new SecurityException("unsupported scheme");
}
String host = uri.getHost();
if (host == null || host.length() > 253) {
throw new SecurityException("invalid host");
}
int port = uri.getPort() == -1 ? 443 : uri.getPort();
if (port != 443) {
throw new SecurityException("unsupported port");
}
InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
if (isNonPublicAddress(address)) {
throw new SecurityException("non-public address");
}
}
// 使用已通过校验的连接策略发起请求;关闭自动重定向
// 同时设置连接超时、读取超时和响应体大小上限其中 isNonPublicAddress 不应只调用一个简单的 isSiteLocalAddress 判断。应根据项目使用的网络库,补充 IPv4 和 IPv6 的回环、链路本地、唯一本地、未指定及保留地址判断。还要注意 DNS 解析结果和实际连接之间的竞态问题,严格场景下应采用固定解析结果的连接实现,或者让受控代理负责解析与出口访问。
七、Python 服务中的实现建议
Python 项目常见问题是直接把用户输入传给 requests.get。至少要明确关闭重定向、设置超时,并在请求前完成地址检查:
from urllib.parse import urlparse
import ipaddress
import socket
def validate_target(value):
parsed = urlparse(value)
if parsed.scheme != "https":
raise ValueError("scheme is not allowed")
if not parsed.hostname or parsed.port not in (None, 443):
raise ValueError("host or port is not allowed")
for item in socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM):
address = ipaddress.ip_address(item[4][0])
if (address.is_private or address.is_loopback or
address.is_link_local or address.is_reserved or
address.is_unspecified):
raise ValueError("non-public address")
return parsed
# 请求时还应设置 timeout、限制响应大小,并根据需要 allow_redirects=False这个示例主要用于说明检查顺序,不应被当作完整的通用防 SSRF 库。生产环境还需要考虑代理环境变量、IPv6 连接、DNS 变化、连接池复用、重定向以及响应体读取方式等因素。
八、代码审计和复现时的安全方法
在授权测试环境中,建议准备三个最小化服务:
- 一个公网模拟资源服务,用于确认正常业务路径。
- 一个仅绑定本机或测试网段的模拟服务,用于验证内网地址是否被阻断。
- 一个返回重定向的测试服务,用于确认每次跳转是否重新校验。
测试时不要连接真实云元数据地址、办公网服务或生产管理接口。可以在本地容器网络中使用无敏感数据的回显服务,并观察应用日志、DNS 查询日志和出口防火墙日志。验证重点是:
| 检查项 | 预期结果 |
|---|---|
| 公网 HTTPS 资源 | 按业务规则正常访问 |
| 回环或私网地址 | 在发起连接前拒绝 |
| 非 HTTPS 协议 | 直接拒绝 |
| 非允许端口 | 直接拒绝 |
| 重定向到受限地址 | 在重定向处理阶段拒绝 |
| 超大响应或慢响应 | 触发大小或时间限制 |
复现记录应包含入口参数、解析结果、最终连接目标、是否发生重定向、响应大小和日志时间线。这样既便于确认漏洞,也有利于后续修复回归和风险定级。
九、常见修复误区
- 只做域名黑名单:黑名单无法覆盖未知内网域名和解析变化,优先采用明确白名单。
- 只检查输入 IP:输入可以是域名,域名解析后的地址才是实际网络目标。
- 只检查第一次请求:重定向后的每一跳都必须重新校验。
- 把请求放到异步任务就认为安全:异步只改变执行时间,不改变网络权限。
- 只依赖应用层校验:应用层可能遗漏边界情况,仍应结合出口网络隔离和防火墙策略。
- 日志记录完整 URL:URL 可能包含令牌、签名或个人数据,应进行脱敏和长度限制。
十、从架构层降低 SSRF 影响
代码修复解决的是输入控制问题,网络架构决定了漏洞一旦遗漏后的影响范围。建议将资源抓取服务放在独立子网,默认拒绝访问内部管理网段;通过固定的出口代理统一执行域名、IP、端口和流量策略;对 DNS 请求、代理访问和异常拒绝建立审计日志。
云环境中还应检查实例角色权限、元数据服务访问策略和容器网络策略。即使业务代码存在缺陷,也不应让普通 Web 服务进程拥有读取高权限元数据或访问管理平面的能力。最小权限、网络分段和凭证短时化,往往比单点的 URL 正则更可靠。
总结
SSRF 的核心不是某个特殊 payload,而是“外部输入影响了服务端的网络边界”。审计时应从数据流出发,沿着 URL 解析、DNS、连接建立、重定向和响应处理逐层确认;修复时不能停留在字符串过滤,而要同时落实协议和端口白名单、地址范围校验、重定向控制、超时限流以及网络隔离。
对于需要长期维护的系统,建议把上述规则封装为统一的安全请求组件,禁止业务代码自行拼接和请求任意 URL,并为公网访问、受信域名访问和完全禁止外连分别提供清晰的接口。这样既能减少重复审计,也能避免同类问题在不同业务模块中反复出现。
推荐意见