SSRF(Server-Side Request Forgery,服务端请求伪造)并不是简单的“后端帮用户发起一次 HTTP 请求”。在实际项目中,漏洞往往来自 URL 解析、域名解析、重定向处理和网络边界控制之间的不一致。单独修补某一个字符串校验规则,通常只能解决一部分问题。本文以代码审计和安全测试为主线,梳理 SSRF 的常见成因、分析方法、可复现的安全测试思路,以及适合落地到生产系统的防护方案。示例均以自有测试环境和无敏感数据的本地服务为前提。一、典型问题场景常见的 SSRF 入口包括图片抓取、远程文件导入、Webhook 调试、URL 预览、PDF 转换、代码仓库镜像和第三方 API 代理等功能。它们通常具有类似的业务逻辑:用户提交 URL
↓
服务端解析参数
↓
服务端发起 HTTP 请求
↓
读取响应并返回或进一步处理如果服务端允许用户完全控制目标 URL,同时没有限制目标网络范围和请求协议,攻击者就可能让服务端访问本不应暴露的资源。风险不一定局限于云环境,也可能包括内网管理面板、服务发现接口、开发环境接口、宿主机本地端口以及带有特殊权限的内部服务。二、代码审计时如何定位 SSRF审计时不要只搜索“requests.get”或“HttpClient”,更有效的方式是从数据流出发,寻找“外部可控输入”到“网络请求函数”的完整路径。入口:HTTP 参数、JSON 字段、消息队列内容、数据库中可被用户写入的 URL。处理:字符串拼接、URL 解码、跳转跟随、代理配置、DNS 解析。汇点:HTTP 客户端、TCP Socket、图片库、文档解析器、浏览器自动化组件。结果处理:是否把响应内容直接返回给用户,是否保存到本地,是否进入模板、解析器或命令执行链路。以下代码在业务上很常见,但存在明显风险:String target = request.getParameter(\"url\");
HttpGet httpGet = new HttpGet(target);
try (CloseableHttpResponse response = client.execute(httpGet)) {
return EntityUtils.toString(response.getEntity());
}问题不只是“没有过滤内网 IP”。该实现还需要确认是否允许 file、ftp 等非 HTTP 协议,是否自动跟随重定向,是否使用代理,是否把 DNS 解析结果缓存,以及响应体是否有大小和类型限制。三、最容易被忽略的解析差异1. 域名校验与实际连接目标不一致一种常见错误是先通过字符串判断域名,再由 HTTP 客户端单独解析并连接。例如代码只判断 URL 是否以 example.com 结尾,这种规则可能把不属于目标域名的主机误判为合法;即使域名本身合法,解析结果也可能在请求前后发生变化。if (url.endsWith(\"example.com\")) {
// 仅凭字符串后缀放行,不能证明目标主机属于 example.com
}更稳妥的做法是先使用标准 URL 解析器提取 host,再进行规范化和精确匹配。对于允许访问的固定服务,优先使用完整主机名白名单,而不是模糊的后缀匹配。2. IPv4、IPv6 与非标准数字表示仅识别常见的点分十进制 IPv4 不够。不同语言库或网络组件可能对十六进制、整数形式、IPv4-mapped IPv6 地址有不同解释。防护代码应将主机名解析为 IP 地址后,统一转换为规范形式,并判断是否属于禁止的地址范围。需要重点检查的范围包括回环地址、链路本地地址、私有地址、未指定地址、组播地址和文档保留地址。IPv6 不能因为没有点号就绕过检查,尤其要关注 ::1、链路本地地址以及 IPv4 映射形式。3. 用户名、端口和特殊字符URL 中的用户名部分容易造成视觉误导。例如看起来像目标域名的字符串,实际主机可能位于后面的 host 字段。解析时应使用标准库读取 scheme、host、port 和 path,不要依赖手工 split 或正则表达式替代 URL 解析。4. 重定向导致的二次访问即使初始 URL 通过了校验,服务端仍可能被 30x 响应引导到另一个地址。如果业务不需要重定向,应关闭自动跟随;如果确实需要,应对每一次跳转重新执行协议、主机、端口和 IP 校验,并限制最大跳转次数。四、DNS 解析与 TOCTOU 问题很多 SSRF 防护采用“先解析域名、检查 IP、再发起请求”的模式,但这中间存在时间窗口。解析检查使用的结果与真正建立连接时使用的结果可能不同,这属于典型的检查与使用之间的竞态问题。生产环境中可以结合以下措施降低风险:对高风险场景使用固定的出口代理,由代理统一执行 DNS 解析和目标策略。禁止客户端自动使用不受控的系统代理和环境变量代理。对每次连接建立前后的目标地址进行记录和校验,避免只在字符串层面判断。设置短连接超时、连接超时和响应读取超时,减少异常解析或恶意服务造成的资源占用。在网络层限制应用服务器访问管理网段、云平台元数据地址和不必要的内网端口。如果使用域名白名单,不能只依赖一次 DNS 结果。对于少量固定目标,更推荐配置解析后的固定地址,或者通过受控代理访问固定上游服务。五、一个相对完整的防护流程下面是适合在服务端实现的检查顺序。具体代码应结合所使用语言和 HTTP 客户端的行为进行验证。1. 限制允许的协议,仅保留 https,必要时再开放 http
2. 使用标准 URL 解析器解析,不使用字符串拼接判断
3. 校验 host 非空,禁止用户名、片段和不必要的特殊端口
4. 将域名解析为全部 IP 地址
5. 拒绝回环、链路本地、私有、未指定和组播地址
6. 校验端口是否属于业务允许范围
7. 关闭或严格控制自动重定向
8. 设置连接、读取、整体请求超时
9. 限制响应大小、响应类型和下载时间
10. 通过网络 ACL 或出口代理执行第二层约束
11. 记录目标域名、解析地址、状态码、耗时和拒绝原因需要强调的是,应用层校验和网络层隔离应当同时存在。应用层负责表达业务意图,网络层负责阻断意外访问,不能把全部安全性寄托在某个正则表达式上。六、Java 实现中的注意事项以 Java 为例,可以先对 URL 进行结构化解析,再对解析结果做地址检查。示例只展示核心思路,实际项目还需要补充 DNS 失败处理、代理配置、连接池和审计日志。URI uri = URI.create(input);
if (!\"https\".equalsIgnoreCase(uri.getScheme())) {
throw new SecurityException(\"unsupported scheme\");
}
if (uri.getUserInfo() != null || uri.getFragment() != null) {
throw new SecurityException(\"unsupported URL component\");
}
if (uri.getHost() == null || uri.getPort() != -1 && uri.getPort() != 443) {
throw new SecurityException(\"invalid host or port\");
}
InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
for (InetAddress address : addresses) {
if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| address.isMulticastAddress()) {
throw new SecurityException(\"private or special address\");
}
}这个示例不能直接视为完整的通用防护代码。不同 JDK、HTTP 客户端和操作系统对 DNS、代理、IPv6 及连接复用的处理存在差异。实际落地时,应在客户端配置层明确关闭不需要的重定向,并在出口网络上配置允许访问的目标范围。七、安全测试方法:优先使用可控的本地环境测试 SSRF 时不应直接探测生产内网。可以在本地或隔离测试网络中启动一个简单的 HTTP 服务,模拟允许访问的上游和拒绝访问的地址,然后验证以下行为:非 HTTP 或 HTTPS 协议是否被拒绝。回环地址、私有地址和 IPv6 特殊地址是否被拒绝。合法域名解析到禁止地址时是否被拒绝。第一次响应发生重定向时,第二个目标是否重新校验。超大响应、慢速响应和异常 Content-Type 是否受到限制。DNS 解析失败、连接超时和证书错误是否会安全失败。可以使用本地服务观察请求是否到达,但不要在测试中携带真实凭据或读取敏感文件。对于 CI 测试,建议将这些用例固化为单元测试和集成测试,避免后续修改 HTTP 客户端配置时重新引入问题。八、出站网络控制比黑名单更可靠黑名单适合做补充,不适合承担主要防护责任。企业环境中更推荐采用默认拒绝的出站策略,仅允许应用访问明确列出的 DNS、IP、端口和代理服务。控制层建议业务层固定上游列表,限制协议、端口、路径和响应大小应用层标准解析、IP 范围校验、禁用不必要重定向、设置超时容器层限制网络命名空间和出站路由,避免应用直接访问管理网段网络层使用 ACL、防火墙或 egress proxy 实施默认拒绝监控层记录目标、解析地址、端口、状态码、耗时和拒绝原因云环境还应单独检查元数据服务的访问控制、实例角色权限和网络策略。即使应用层存在缺陷,云平台侧的最小权限和元数据访问保护也能降低影响范围。九、日志与事件复盘重点SSRF 事件复盘时,不能只看攻击者提交的原始 URL。建议同时关联以下信息:原始输入与规范化后的 URL。最终解析到的全部 IP 地址。是否经过代理、是否发生重定向。实际连接端口、响应状态码和响应大小。请求耗时、超时类型和异常堆栈。应用身份、容器或主机标识以及对应的网络策略。日志中的 URL 参数可能包含令牌、签名或用户隐私,记录时应进行脱敏。对于被拒绝的请求,保留结构化的拒绝原因通常比完整保存敏感参数更有价值。十、审计清单是否存在由用户控制目标地址的服务端请求功能。是否只允许业务需要的协议和端口。是否使用标准解析器,而不是字符串截取或简单正则。是否同时校验域名、解析后的全部 IP 和最终连接目标。是否处理 IPv6、重定向、代理和 DNS 变化。是否限制连接超时、读取超时、跳转次数和响应体大小。是否通过网络策略阻止应用访问管理面、元数据服务和不必要的内网段。是否有覆盖异常地址、重定向和超时场景的自动化测试。总结SSRF 的本质是“不受信任的输入影响了服务端的网络边界”。有效防护不能停留在过滤几个字符串或屏蔽一个内网地址,而应从 URL 解析、DNS、HTTP 客户端、重定向、资源限制和网络出口多个层面建立约束。从代码审计角度,最重要的是追踪输入到网络请求的完整数据流;从修复角度,优先采用固定目标白名单、默认拒绝的出站策略和受控代理;从复盘角度,则要确认最终连接地址和实际网络权限,而不是只看表面的 URL 参数。将这些检查固化到代码、配置和自动化测试中,才能避免同类问题在后续迭代中反复出现。
推荐意见