SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“让服务器代替用户访问一个 URL”这么简单。真正危险的地方在于:服务端通常处于比普通用户更宽的网络边界内,可能访问内网管理面板、服务发现接口、容器控制面或云环境的身份凭证服务。这类漏洞的难点也不只在于找到一个发起 HTTP 请求的参数,而在于确认请求的完整链路:输入是否可控、协议是否受限、域名是否解析到内网地址、是否允许跳转、出口网络是否隔离,以及响应内容最终是否会回显给攻击者。本文以代码审计和授权测试为主线,整理一套可复用的 SSRF 分析方法,并重点讨论修复时经常被忽略的边界条件。一、典型场景与风险边界常见的 SSRF 入口包括以下功能:头像、附件、网页截图或远程图片导入;Webhook、回调地址、第三方通知配置;URL 预览、文档转换、RSS 抓取和站点连通性检测;服务端代理、图片转码、PDF 生成和远程日志采集;根据用户提交的地址读取远程配置或同步数据。风险通常分为三类。第一类是内网探测,服务端可以访问用户无法直接访问的管理端口。第二类是敏感信息读取,例如错误配置、内部接口响应或运行环境中的身份信息。第三类是进一步影响内部服务的安全性,例如向缺少认证的管理接口提交请求。在实际评估中,不应把“能否访问某个内部地址”作为唯一判断标准。即使响应内容不可回显,只要服务器能够向内网发起请求,也可能构成盲 SSRF,需要通过受控的回连日志或网络侧观测进行确认。二、代码审计:先找请求汇聚点,再追溯输入来源审计时建议从真正发起网络请求的 API 反向追踪,而不是只搜索参数名。不同项目中,危险调用可能被封装在 HTTP 客户端、文件下载器、图片处理器或异步任务中。Java 项目中可以重点关注 URL.openConnection、HttpURLConnection、Apache HttpClient、OkHttp,以及封装后的 RestTemplate、WebClient。Node.js 项目则重点关注 http.request、https.request、fetch、Axios 等调用点。Python 服务常见的汇聚点包括 requests、urllib 和异步 HTTP 客户端。一条完整的数据流通常是:用户参数
-> Controller / Route
-> 参数校验与规范化
-> URL 解析
-> DNS 解析
-> HTTP 客户端
-> 重定向处理
-> 响应读取与回显审计记录至少应包含以下字段:输入是否完全由用户控制,还是只能选择预置域名;是否限制了协议,仅允许 http 和 https;URL 解析使用的库和版本,是否存在多重解析差异;域名解析发生在哪里,是否在校验前和连接时分别解析;是否自动跟随 30x 跳转,跳转后的地址是否重新校验;响应是否回显、是否落盘、是否进入后续解析流程;请求是否经过统一出口代理,网络层是否有最小权限控制。三、URL 校验最容易出现的逻辑错误1. 只检查字符串前缀下面这种判断不能作为安全校验:if (url.startsWith(' {
fetch(url);
}字符串前缀并不等价于主机名匹配。一个看起来以可信域名开头的字符串,后面仍可能包含用户控制的主机部分。正确做法是先使用标准 URL 解析器,再读取解析后的协议、主机名和端口,禁止直接对原始字符串做拼接判断。2. 只禁止几个内网地址段简单屏蔽少数 IPv4 网段往往不够。至少应同时考虑:IPv4 私有地址、环回地址、链路本地地址和保留地址;IPv6 环回地址、链路本地地址、唯一本地地址;IPv4-mapped IPv6 等表示形式;解析结果中包含多个地址的情况;域名在校验阶段和连接阶段解析结果发生变化的情况。更稳妥的策略不是建立一份不断补丁式扩展的黑名单,而是使用明确的允许列表:只允许访问业务实际需要的域名、协议和端口,并在网络层再次限制出口。3. 校验域名,连接时却重新解析这是 DNS 重绑定类问题的核心。应用可能先解析域名并判断结果是公网地址,随后 HTTP 客户端再次解析同一域名,最终连接到了不同的地址。仅在业务代码中调用一次 DNS 查询,并不能保证后续连接使用的就是这次结果。修复时需要确认 HTTP 客户端的连接流程,尽量让“解析、地址校验、建立连接”处于同一受控链路中。对于高风险场景,可以使用固定的解析器、缓存经过校验的地址,或通过出口代理统一执行域名解析和目标地址策略。4. 忽略重定向即使第一跳地址通过校验,远端服务仍可能返回指向其他主机的 30x 响应。如果客户端默认跟随跳转,最终请求目标可能完全脱离原有允许范围。处理方式有两种:高风险功能直接禁用自动跳转;确需支持跳转时,对每一跳的协议、主机、端口和解析结果重新执行完整校验,并限制最大跳转次数。还要注意部分客户端会自动处理相对跳转、协议降级和跨协议跳转。四、一个相对完整的安全校验思路下面的示例使用 Java 展示校验流程。示例重点是思路,不建议直接复制到所有项目中使用。生产环境还应结合统一网络出口、审计日志和业务允许列表进行设计。URI target = URI.create(input);
String scheme = Optional.ofNullable(target.getScheme())
.orElse('')
.toLowerCase(Locale.ROOT);
if (!Set.of('http', 'https').contains(scheme)) {
throw new IllegalArgumentException('unsupported scheme');
}
if (target.getUserInfo() != null) {
throw new IllegalArgumentException('userinfo is not allowed');
}
String host = target.getHost();
if (host == null || host.isBlank()) {
throw new IllegalArgumentException('invalid host');
}
int port = target.getPort();
if (port != -1 && port != 80 && port != 443) {
throw new IllegalArgumentException('unsupported port');
}
if (!allowedHost(host)) {
throw new IllegalArgumentException('host is not allowlisted');
}
for (InetAddress address : InetAddress.getAllByName(host)) {
if (isNonPublicAddress(address)) {
throw new IllegalArgumentException('non-public address');
}
}这个示例仍有几个需要补充的地方:allowedHost 应采用规范化后的主机名进行精确匹配或明确的子域匹配,不能使用简单后缀匹配;isNonPublicAddress 需要覆盖 IPv4、IPv6 和映射地址;真正发起连接时还要防止 DNS 重新解析带来的地址漂移。如果业务只需要从固定的对象存储域名下载文件,更推荐把参数设计成对象 ID 或内部资源标识,而不是允许用户提交任意 URL。减少输入自由度,通常比在任意 URL 上堆叠规则更可靠。五、响应处理同样决定漏洞等级SSRF 的影响不仅取决于能访问哪里,还取决于服务端如何处理响应。直接把响应正文返回给用户,属于高风险的回显型 SSRF;只返回状态码、长度或固定错误信息,通常降低了信息泄露程度,但仍可能存在盲 SSRF;把响应保存为图片、PDF 或 XML 后继续解析,可能叠加文件格式解析、XXE、反序列化等问题;把远端内容写入缓存、日志或数据库,可能形成存储型风险和二次解析风险。远程资源处理应设置连接超时、读取超时、最大响应大小和最大响应头数量,并限制内容类型。对于图片、文档等文件,不要只信任远端返回的 Content-Type,应在隔离环境中进行格式识别和转换。connectTimeout: 2s
readTimeout: 5s
maxResponseBytes: 5MB
maxRedirects: 0
allowedContentTypes:
- image/png
- image/jpeg超时和大小限制不仅是稳定性要求,也能降低通过慢响应、大文件或资源耗尽方式影响服务的风险。六、授权测试中的安全复现方法在测试环境中,优先使用自己控制的回连服务或本地模拟服务,不要直接探测生产内网、云平台管理接口或第三方系统。验证目标是证明“服务端确实发起了请求”,而不是扩大访问范围。可以在隔离网络中启动一个只记录请求时间、来源地址、路径和请求头摘要的测试端点,并使用测试数据调用业务功能。例如:python3 -m http.server 8000 --bind 127.0.0.1如果需要验证跳转处理,可在本地准备两个测试端点:第一个返回固定的 302 响应,第二个记录是否收到请求。测试时只观察应用是否遵循跳转、是否重新执行校验,以及日志中是否能关联原始任务和最终目标。对于盲 SSRF,可以使用组织内部批准的回连记录服务,或者在实验网络中通过 DNS 和 HTTP 日志确认请求到达。报告中应记录测试时间、请求标识、目标为测试端点这一事实,以及没有访问生产敏感资源的限制。七、网络层防护不能被应用层校验替代应用层校验存在实现差异和维护成本,网络层应作为第二道边界。将远程抓取任务放入独立 worker 或沙箱,不要让主业务进程直接访问内网;默认拒绝到 RFC1918、环回、链路本地、云管理网段和内部 DNS 的出站连接;只允许访问业务所需的目标域名和端口;统一通过出口代理执行 DNS 解析、地址策略和审计;禁止服务账号访问不必要的本地凭证文件、Unix Socket 和容器运行时接口;对异常的出站请求进行日志记录和告警,包括目标域名、解析地址、端口、响应状态和任务来源。网络策略应和应用功能一起评审。例如某个图片导入服务只需要访问公开的图片 CDN,那么它不应拥有访问数据库网段、服务注册中心或内部控制平面的权限。八、修复验证清单修复完成后,不能只用一个正常 URL 验证。建议至少覆盖以下测试类别:合法的业务域名、合法端口和合法文件类型;缺少协议、协议大小写变化、空主机名和非法端口;用户名信息、片段、重复端口和异常编码形式;解析到环回、私有、链路本地和 IPv6 本地地址的域名;一个域名对应多个地址,其中混入不可访问地址;第一跳合法、第二跳超出允许范围的重定向;超时、超大响应、错误内容类型和连接中断;异步任务、缓存任务和重试机制是否绕过了同一套校验。如果系统采用域名允许列表,还应验证大小写、尾随点、国际化域名和规范化规则是否一致。允许列表的实现最好集中在一个经过测试的组件中,避免不同业务各自编写一套近似但不一致的逻辑。九、审计和报告中的常见误区第一,不要把“参数名叫 url”直接等同于 SSRF。需要追到实际网络请求,并确认请求由服务器发起。第二,不要把只能访问公网的远程抓取功能自动判为严重漏洞,应结合是否存在回显、是否能跳转、是否能访问敏感服务以及网络隔离情况进行定级。第三,不要只报告一组输入样例,应说明根因是缺少目标约束、未校验解析结果、未处理跳转,还是网络出口权限过宽。报告中的复现步骤应尽量使用受控目标,并明确测试边界。修复建议也应同时覆盖代码、客户端配置和网络策略,否则单纯增加字符串黑名单很容易在后续迭代中再次失效。总结SSRF 审计的核心不是寻找某个“神奇字符串”,而是还原服务端从输入到连接的完整行为。可靠的防护通常包含四层:业务层减少任意 URL 输入,优先采用资源标识和固定目标;解析层使用标准 URL 解析器,严格限制协议、主机、端口和地址类型;连接层处理 DNS 变化、重定向、超时和响应大小;网络层采用默认拒绝的出站策略,并将高风险抓取任务隔离。从长期维护角度看,允许列表、统一出口和最小网络权限比不断增加黑名单更具稳定性。漏洞修复完成后,还应把关键用例加入自动化测试,并在日志和网络监控中保留足够的审计信息,这样才能避免同类问题在新功能中重复出现。
推荐意见