Spring Cloud Gateway 基于 WebFlux 构建,底层大量使用 SpEL(Spring Expression Language)实现路由断言和过滤器逻辑。攻击者一旦能向 Gateway 的 Actuator 端点注入恶意 SpEL 表达式,就可以直接执行任意系统命令,这就是 CVE-2022-22947 等一系列高危漏洞的核心成因。问题的根源在于 Gateway 默认暴露的 Actuator 端点允许动态添加或修改路由,而路由中的 filters 和 predicates 参数直接接受 SpEL 表达式,服务端并未对这些表达式做充分的安全校验。
修复这类漏洞不能只靠升级版本,因为攻击手法在不断变异。真正有效的防护需要从架构层面切断攻击链:禁用不必要的 Actuator 端点、对路由配置实施严格的输入校验、以及通过安全配置限制 SpEL 的表达能力。下面直接拆解具体的攻击路径和对应的硬核防护措施。
攻击者如何通过 SpEL 注入拿下服务器攻击的第一步通常是探测 Gateway 是否暴露了 Actuator 端点。默认情况下,/actuator/gateway/routes 这个端点可以列出所有路由信息,而 /actuator/gateway/routes/{id} 则允许创建或刷新路由。如果这两个端点没有做访问控制,攻击者就可以向 Gateway 发送一个精心构造的 POST 请求,创建一个包含恶意 SpEL 表达式的路由。
恶意路由的典型结构如下:攻击者在 filters 数组中添加一个 AddResponseHeader 或 RewritePath 过滤器,在对应的 value 字段里嵌入 SpEL 表达式。表达式的内容通常是 T(java.lang.Runtime).getRuntime().exec("command") 这类调用,利用 SpEL 的类型引用和静态方法调用能力直接执行系统命令。由于 Gateway 在解析路由配置时会自动对 SpEL 表达式求值,恶意代码在路由被创建或刷新的瞬间就会触发执行,不需要任何额外的用户请求触发。
更隐蔽的攻击方式是利用 Gateway 的 RefreshRoutes 事件。攻击者可以先通过 POST 创建一个恶意路由,然后向 /actuator/gateway/refresh 端点发送 POST 请求,强制 Gateway 重新加载路由定义。这个刷新过程会重新解析所有路由的 SpEL 表达式,再次触发恶意代码执行。即便 Gateway 在创建路由时做了某种程度的检查,刷新路径也可能绕过这些检查。
第一步:彻底关闭不必要的 Actuator 端点最直接的防护手段是禁用 Gateway 的 Actuator 端点,或者至少禁用与路由管理相关的端点。在 application.yml 或 application.properties 中,将 gateway 相关的 actuator 端点全部关闭:
management:
endpoints:
web:
exposure:
exclude: gateway
endpoint:
gateway:
enabled: false
这段配置会禁用 /actuator/gateway 下的所有端点,包括 routes、refresh、globalfilters 等。如果业务确实需要通过 API 动态管理路由,那么至少要确保这些端点有严格的认证和鉴权,不能直接暴露在公网上。通常的做法是集成 Spring Security,对 Actuator 端点实施角色访问控制,只允许特定内网服务调用。
第二步:限制 SpEL 表达式的执行能力即使 Actuator 端点被保护,仍然需要在表达式求值层面做防御。Gateway 内部使用 StandardEvaluationContext 来解析 SpEL,这个上下文默认开放了所有 Java 类的访问权限。防御的关键是使用 SimpleEvaluationContext 替代 StandardEvaluationContext,或者在自定义的 EvaluationContext 中禁用类型引用和构造器调用。
Spring 官方在修复 CVE-2022-22947 时,就在 Gateway 的表达式求值逻辑中引入了更严格的上下文限制。但如果你使用的是较旧版本或者自定义了过滤器,需要自行确保所有涉及 SpEL 求值的地方都使用了受限的上下文。SimpleEvaluationContext 只允许访问显式注册的变量和方法,默认情况下禁止 T() 类型引用和 new 构造器调用,这能从根本上阻断命令执行链。
对于必须使用复杂 SpEL 表达式的场景,可以自定义一个 EvaluationContext,通过 setTypeLocator 和 setConstructorResolvers 方法精确控制可访问的类型和白名单。例如,只允许访问 java.lang.Math 等安全的工具类,彻底禁止 java.lang.Runtime 和 java.lang.ProcessBuilder 等危险类型。
第三步:对路由配置参数做严格的输入校验Gateway 的路由定义本质上是一组结构化的 JSON 对象。如果业务允许通过 API 动态创建路由,那么必须在接收这些 JSON 数据时进行严格的格式和内容校验。校验的重点不是简单的字符串过滤,而是对 SpEL 表达式的语法特征进行识别和拦截。
具体的校验规则包括:禁止输入中包含 T( 字符序列,因为这是 SpEL 调用静态方法的标志;禁止包含 new 关键字和 getRuntime、exec 等危险方法名;限制表达式只能使用预定义的变量名,如 #request、#response 等 Gateway 内置变量。可以使用正则表达式对输入进行多层过滤,但要注意攻击者可能会使用 Unicode 编码或字符串拼接来绕过简单的正则匹配。
更可靠的做法是在路由创建接口层实现白名单机制:只允许用户选择预定义的过滤器类型和参数模板,不允许直接输入完整的 SpEL 表达式。如果业务需求确实需要用户自定义表达式,那就必须在服务端对表达式进行静态分析,解析 AST 树并检查其中是否包含危险节点。
第四步:加固 SpEL 表达式解析器本身Gateway 的 SpEL 解析发生在多个组件中,包括 RoutePredicateFactory 和 GatewayFilterFactory 的实现类。这些工厂类在解析路由定义时,会调用 getValue 方法从配置中提取 SpEL 表达式并求值。如果使用的是 Spring Cloud Gateway 3.0.7 之前的版本,这些解析器默认使用 StandardEvaluationContext,需要立即升级到修复版本。
对于无法升级的场景,可以通过自定义工厂类来覆盖默认的解析行为。例如,重写 RewritePathGatewayFilterFactory 的 apply 方法,在内部使用受限的 EvaluationContext 进行表达式求值。同时,在全局层面注册一个 GatewayFilter 拦截器,对所有经过 Gateway 的请求和路由配置进行二次校验,确保没有恶意的 SpEL 片段被注入到运行时环境中。
第五步:网络层和运行时环境的纵深防御即使应用层做了充分防护,也应该在运行时环境施加额外的限制。运行 Gateway 的 JVM 进程应使用最小权限原则,以非 root 用户身份运行,并通过 SecurityManager 或容器安全策略限制进程可以执行的系统调用。在 Kubernetes 环境中,为 Gateway Pod 配置 SecurityContext,禁止 privilege escalation,并挂载只读根文件系统。
网络层面,Gateway 的 Actuator 端口应该与业务端口分离,且 Actuator 端口只监听内网地址。在反向代理或负载均衡器上配置规则,直接丢弃所有发往 /actuator/gateway 路径的外部请求。同时,部署 WAF 规则检测请求体中是否包含 SpEL 的典型攻击特征,如 T(、Runtime、exec 等模式,作为应用层防护的补充。
监控和应急响应防护措施部署后,需要建立对 SpEL 注入攻击的监控能力。在 Gateway 的日志中开启 DEBUG 级别,记录所有路由变更操作和表达式求值过程。使用日志分析工具实时检测异常的路由创建请求,特别是那些包含 T( 或 getRuntime 的请求体。同时,监控 Gateway 进程的系统调用行为,一旦发现 Java 进程尝试执行 bash、sh、curl 等外部命令,立即触发告警并阻断进程。
应急响应方面,预先准备好 Gateway 配置的回滚脚本。一旦检测到恶意路由被注入,可以通过 /actuator/gateway/routes/{id} 的 DELETE 请求快速删除恶意路由,然后执行 refresh 刷新配置。更彻底的应急措施是直接重启 Gateway 实例,因为内存中的恶意路由在重启后会消失,前提是持久化存储中的配置没有被污染。
整个防护体系的核心思路是:默认不信任任何外部输入,将 SpEL 的求值能力限制在最小必要范围内,并在多个层面建立纵深防御。单纯依赖官方补丁或某一种防护手段,在面对变种攻击时仍然可能失守。
