当前位置: 首页 > news >正文

Spring SpEL表达式注入漏洞:原理、审计与修复实战指南

1. 项目概述:为什么SpEL表达式注入值得警惕

最近在帮几个团队做代码安全审计,发现一个挺有意思的现象:很多开发同学对SQL注入、XSS这些传统漏洞的防范意识已经很强了,框架层面也做了不少防护。但一提到表达式注入,特别是Spring框架里的SpEL(Spring Expression Language)表达式注入,不少人就有点懵,要么是完全没概念,要么是知道有风险但不知道具体在哪、怎么防。这其实挺危险的,因为SpEL功能强大,用得好是利器,用不好就是给系统埋了个大雷。

简单来说,SpEL是Spring框架提供的一套功能强大的表达式语言,它允许你在运行时动态地查询和操作对象图。它的设计初衷是为了让Spring的配置文件、注解(比如@Value@PreAuthorize)以及一些模板(如Thymeleaf)更灵活。你可以用它来调用方法、访问属性、进行数学运算、逻辑判断等等。问题就出在这个“动态”和“强大”上。如果开发人员不小心将用户可控的输入,未经任何过滤或校验,直接拼接到了SpEL表达式的上下文中并执行,攻击者就能构造恶意的表达式,实现远程代码执行(RCE)。这个危害级别和Struts2的OGNL表达式注入、Log4j的JNDI注入是一个量级的。

为什么现在要特别关注它?一方面,Spring Boot/Cloud的普及让SpEL的使用场景大大增加,不仅仅是配置,在Spring Security的权限注解、缓存Key的生成、甚至一些消息转换的逻辑里都可能用到。另一方面,这种漏洞往往比较隐蔽,它不像SQL注入那样有明显的数据库操作语句,可能就藏在一个@Value("#{systemProperties['user.region']}")这样的注解里,或者一个StandardEvaluationContext的用法中。对于做代码审计或者安全开发的同学来说,理解SpEL的机制,并能在代码中精准地定位和评估这类风险,是一项越来越重要的基本功。

2. SpEL表达式注入的核心原理与风险场景拆解

要审计,先得懂原理。SpEL注入的本质和大多数注入漏洞一样:“信任了不可信的数据”。SpEL引擎在解析表达式时,如果表达式的内容可以被外部输入控制,并且解析器使用了功能强大的StandardEvaluationContext(这是默认且功能最全的上下文),那么攻击者就能突破表达式的原本意图,执行任意代码。

2.1 SpEL的两种关键解析上下文

这是理解风险的核心。SpEL主要使用两种EvaluationContext

  1. StandardEvaluationContext:这是功能完整的上下文。它提供了对SpEL语言全部特性的支持,包括但不限于:

    • 创建新对象(new String('xxx'))。
    • 调用任意方法('abc'.toUpperCase())。
    • 引用类型(T(java.lang.Runtime))。
    • 访问静态方法和属性(T(java.lang.Runtime).getRuntime())。
    • 赋值、类型转换等。风险点:一旦用户输入能进入这个上下文的表达式,几乎等同于拥有了执行任意Java代码的能力。这是高危漏洞的根源。
  2. SimpleEvaluationContext:这是Spring 4.2+引入的、功能受限的上下文。它被设计用于只需要数据绑定、简单属性查询或条件判断的场景。它明确禁止了以下操作:

    • 引用Java类型(T(...))。
    • 构造函数调用(new)。
    • 方法调用(除非显式配置允许)。
    • 赋值操作。安全建议:在绝大多数从外部(如HTTP参数、配置文件、数据库)获取表达式内容的场景下,都应该使用SimpleEvaluationContext,它能从根本上杜绝代码执行。

很多历史漏洞和错误用法,都是因为在不该用StandardEvaluationContext的地方用了它,或者该用SimpleEvaluationContext时没意识到它的存在。

2.2 高风险代码模式与常见触发点

在代码审计时,你需要像侦探一样寻找这些“犯罪模式”。我总结了几类最常见的风险点:

模式一:动态解析用户输入的表达式这是最直接、最危险的模式。代码直接接收用户输入,并交给SpEL引擎解析。

// 危险示例:直接解析用户输入 @GetMapping("/eval") public String eval(@RequestParam String expression) { ExpressionParser parser = new SpelExpressionParser(); // 这里使用了默认的StandardEvaluationContext! Expression exp = parser.parseExpression(expression); return exp.getValue().toString(); }

攻击者传入T(java.lang.Runtime).getRuntime().exec('calc'),服务器就可能弹出计算器(视操作系统而定)。审计时,看到SpelExpressionParser().parseExpression()且参数外部可控,就要立刻拉响警报。

模式二:注解中的动态表达式Spring的@Value注解非常方便,但也可能成为隐患。当注解的值来自配置文件,而配置文件的值又可能被外部篡改(如环境变量、配置中心)时,风险就产生了。

// 潜在风险:配置来源不可信 @Component public class SomeService { @Value("${custom.expression:default}") // 假设custom.expression来自外部配置 private String dynamicValue; }

如果custom.expression被恶意设置为一个SpEL表达式,在Spring容器初始化解析@Value时就会执行。审计配置中心、环境变量传递链的安全性同样重要。

模式三:Spring Security 权限表达式Spring Security的@PreAuthorize@PostAuthorize等注解支持SpEL,用于复杂的权限判断。

// 风险示例:权限表达式掺入用户输入 @PreAuthorize("hasPermission(#document, 'write')") public void updateDocument(Document document) { // ... }

这个例子本身是安全的,因为它使用的是Security内置的方法和参数。但如果开发人员错误地拼接了用户输入,比如@PreAuthorize("hasRole('" + userInput + "')"),就会造成注入。需要审计所有权限注解中的表达式,检查是否有字符串拼接操作。

模式四:缓存Key的生成一些缓存框架(如Spring Cache)允许使用SpEL来生成缓存的Key。

// 风险示例:缓存Key包含未过滤的用户输入 @Cacheable(value="books", key="#isbn + #userInput") public Book findBook(String isbn, String userInput) { // ... }

这里的key参数是一个SpEL表达式,如果#userInput是用户可控的,攻击者就可以注入恶意表达式。需要检查所有@Cacheable@CachePut等注解的keycondition属性。

模式五:XML配置文件中的SpEL在老式的基于XML的Spring配置中,SpEL也广泛应用。

<bean id="myBean" class="com.example.MyBean"> <property name="value" value="#{systemProperties['user.home']}" /> </bean>

如果这里的systemProperties['user.home']或其部分来自不可信的配置源,同样存在风险。审计旧项目时,需要仔细检查XML配置文件。

注意:并非所有使用StandardEvaluationContext的地方都一定有漏洞。关键判断依据是:表达式字符串是否全部或部分由外部不可信输入控制?如果表达式是开发人员硬编码在源码中的固定字符串(如@Value("#{systemProperties['java.version']}")),那么风险是可控的(除非攻击者能篡改系统属性)。风险在于“动态拼接”。

3. 代码审计实战:手工与工具结合挖掘漏洞

知道了原理和模式,我们就可以开始实战审计了。我习惯采用“静态扫描(工具)+ 动态分析(手工)”相结合的方式。

3.1 静态代码扫描:快速定位可疑点

首先,利用工具进行大范围筛查,提高效率。

  1. 使用IDE的搜索功能:这是最直接的方法。在IntelliJ IDEA或Eclipse中,全局搜索以下关键词:

    • SpelExpressionParser
    • parseExpression
    • StandardEvaluationContext
    • @Value(并检查其值是否包含#{}${},且来源可疑)
    • @PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter
    • @Cacheable,@CachePut,@CacheEvict(检查key/condition)
    • EvaluationContext将搜索到的结果逐一列入待审查清单。
  2. 使用专用SAST工具:很多商业或开源的静态应用安全测试工具已经支持SpEL注入的检测规则。例如:

    • SonarQube: 自定义或使用现有的安全规则包,可以扫描出潜在的表达式注入问题。
    • SpotBugs/Find Security Bugs: 这是一个非常棒的免费开源插件。Find Security Bugs插件包含了“SPEL_INJECTION”的检测规则,能直接标识出高危的SpelExpressionParser.parseExpression()调用点。
    • Fortify SCA, Checkmarx: 商业工具通常也有较强的检测能力。操作心得:不要完全依赖工具的报错。工具可能会误报(将安全的动态表达式标记为问题)或漏报(无法识别复杂的字符串拼接路径)。工具的定位是“辅助”,帮你缩小范围,最终的判断需要人工进行。

3.2 人工代码审计:深度分析与确认

拿到工具扫描出的可疑点后,开始深入的人工分析。这是最考验功力的环节。

第一步:数据流溯源对于每一个可疑的SpEL解析调用,画出一条简单的数据流图:

用户输入 (HTTP参数/Header/Cookie/文件) -> 控制器(Controller) -> 服务层(Service) -> SpEL解析点

你需要回答:那个最终传入parseExpression()的字符串,它的每一个部分都是从哪来的?有没有用户可控的部分?例如:

public String evaluate(String userInput) { String prefix = "T(java.lang.System).getProperty('"; String suffix = "')"; // 危险!直接拼接 String expression = prefix + userInput + suffix; return parser.parseExpression(expression).getValue(String.class); }

这里很明显,userInput被直接拼接进了表达式。但如果数据流很长、很绕,可能需要跟踪多个方法调用。

第二步:上下文分析确认解析时使用的EvaluationContext类型。

  • 如果看到是new StandardEvaluationContext()或者parser.parseExpression(...)(默认即Standard),且表达式可控,基本可以判定为高危漏洞
  • 如果看到SimpleEvaluationContext.forReadOnlyDataBinding().build(),那么风险极低,但仍需确认是否配置了不该有的权限(如允许方法调用)。

第三步:评估利用难度与影响确认漏洞后,评估其实际可利用性:

  • 出网情况: 漏洞点所在的服务器是否能访问外部网络?这决定了攻击者是否能轻易地下载并执行远程恶意代码。
  • 权限限制: 执行SpEL的Java进程是以什么权限运行的?高权限(如root/Administrator)会放大危害。
  • 输入限制: 前端或参数层面对输入是否有长度、字符类型的限制?可能会阻碍复杂payload的注入。
  • 触发条件: 漏洞接口是公开的还是需要认证?触发频率如何?

第四步:构造验证Payload(在授权测试环境下!)为了最终确认漏洞,需要构造一个无害的验证payload。绝对不要在生产环境尝试执行rm -rfformat之类的命令!

  1. 延迟验证: 利用Thread.sleep()来制造一个可观察的延迟,这是最安全的方式之一。
    T(java.lang.Thread).sleep(5000)
    发送请求后,观察响应时间是否明显增加了5秒。
  2. DNS外带验证: 如果怀疑有出网限制,可以尝试触发DNS查询,这是很多防火墙默认放行的协议。
    T(java.lang.Runtime).getRuntime().exec('nslookup your-controlled-domain.com')
    在你的DNS日志平台上查看是否有查询记录。
  3. 无害系统信息读取
    T(java.lang.System).getProperty('user.dir')
    T(java.lang.Runtime).getRuntime().availableProcessors()
    检查返回内容是否与预期相符。

重要提醒:所有漏洞验证必须在获得明确书面授权的测试环境中进行。未经授权的测试是违法的。

3.3 审计案例实录:一个真实的缓存Key注入

我曾审计过一个Spring Boot电商项目,发现了这样一个案例:

@Service public class ProductService { @Cacheable(value = "products", key = "'product_' + #id + '_' + #filterParams") public Product getProductWithFilter(Long id, String filterParams) { // ... 查询数据库 } }

@Cacheablekey属性是一个SpEL表达式。这里的#filterParams是方法参数,而前端搜索框的内容会直接传给这个参数。看起来filterParams可能是像“color=red&size=M”这样的字符串,用于缓存不同的筛选结果。

风险分析

  1. 数据流:用户在前端输入 -> 作为filterParams传入 -> 直接拼接到缓存Key的SpEL表达式中。
  2. 上下文:Spring Cache默认使用StandardEvaluationContext来解析key表达式。
  3. 漏洞确认:我构造了这样一个请求:id=1&filterParams=T(java.lang.Runtime).getRuntime().exec('calc')。由于表达式被拼接为'product_' + 1 + '_' + T(java.lang.Runtime).getRuntime().exec('calc'),SpEL引擎执行了这段代码,成功触发了计算器程序。这是一个标准的SpEL注入导致RCE的案例。

修复方案:修复方式不是过滤filterParams(很难过滤全面),而是避免将其直接用于表达式。可以改为在Service层内部,使用安全的字符串操作生成一个纯粹的字符串作为缓存Key的一部分,或者直接使用SimpleEvaluationContext(但需Spring Cache支持,更常见的修复是避免动态表达式)。

4. 漏洞修复方案与安全编码实践

找到漏洞只是第一步,给出正确、可实施的修复方案才是价值所在。针对不同的场景,修复策略也不同。

4.1 根本解决方案:使用SimpleEvaluationContext或禁用动态特性

这是最推荐、最根本的修复方式。

场景:自定义的SpEL解析逻辑如果你的代码中自己实例化了SpelExpressionParser来解析动态内容,务必使用SimpleEvaluationContext

// 修复后:使用SimpleEvaluationContext ExpressionParser parser = new SpelExpressionParser(); // 创建只支持属性访问、类型转换等基本操作的上下文 SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); // 假设expression来自可信或严格过滤的来源,或者本身就是安全的 Object result = parser.parseExpression(safeExpression).getValue(context, null);

通过SimpleEvaluationContext,即使表达式被恶意注入,攻击者也无法调用方法、构造对象或访问类型,从而彻底杜绝代码执行。

场景:无法避免StandardEvaluationContext在极少数情况下,业务确实需要StandardEvaluationContext的完整功能(例如,一个内部使用的规则引擎)。那么,必须实施严格的输入白名单校验

  • 语法树白名单: 使用SpEL的SpelNode对表达式进行解析,遍历语法树,只允许特定的节点类型(如属性引用PropertyOrFieldReference、字面量Literal、操作符Op等),禁止MethodReferenceConstructorReferenceTypeReference等危险节点。
  • 正则表达式白名单: 对于非常简单的场景,可以用严格的正则表达式来匹配只允许出现的字符集(如仅数字、字母、下划线、点号),但这通常不够安全,因为SpEL语法复杂。

4.2 场景化修复指南

  1. 修复@Value注解风险

    • 评估来源:检查${}占位符的值来源(如application.properties、环境变量、配置中心)。确保这些配置源本身是安全的,不会被未授权修改。
    • 避免SpEL:如果不需要SpEL功能,对于外部配置,尽量直接使用@Value("${prop}")而不加#{}。如果需要SpEL,确保表达式是静态的或仅引用可信的内部属性。
  2. 修复Spring Security表达式风险

    • 严禁拼接:绝对不要在@PreAuthorize等注解的字符串中进行用户输入的字符串拼接。
    • 使用参数:利用Spring Security SpEL内置的对象和方法(如principalauthenticationhasRole()hasPermission())以及方法参数(#id)来构建表达式。这些是安全的,因为它们是框架管理的对象。
    // 安全:使用框架提供的安全对象和方法参数 @PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username") public void updateUserInfo(String username) { ... }
  3. 修复Spring Cache Key注入风险

    • 自定义KeyGenerator:这是最优雅的解决方案。实现一个KeyGenerator接口的Bean,在其中用安全的代码生成缓存Key,完全避开SpEL。
    @Configuration public class CacheConfig { @Bean public KeyGenerator safeKeyGenerator() { return (target, method, params) -> { // 安全地生成Key,例如使用MD5哈希参数 StringBuilder sb = new StringBuilder(method.getName()); for (Object param : params) { sb.append(Objects.toString(param)); } return sb.toString(); }; } } // 使用 @Cacheable(value="books", keyGenerator="safeKeyGenerator") public Book findBook(String isbn, String filter) { ... }
    • 手动生成Key字符串:在Service方法内部,生成一个明确的字符串作为Key,而不是依赖动态SpEL。
    @Cacheable(value="products", key="#root.target.generateKey(#id, #filterParams)") public Product getProduct(Long id, String filterParams) { ... } // 在同一个类中定义生成Key的方法 public String generateKey(Long id, String filterParams) { // 在这里进行安全的字符串操作,或者对filterParams进行严格校验/编码 return "product_" + id + "_" + URLEncoder.encode(filterParams, StandardCharsets.UTF_8); }

4.3 安全编码红线

  • 红线一:永远不要将任何形式的用户输入(HTTP参数、Header、Cookie、文件内容、数据库字段、第三方API返回)直接拼接进SpEL表达式字符串。
  • 红线二:在解析来自外部的、非完全可信的表达式时,默认使用SimpleEvaluationContext。只有在你百分之百确定表达式来源和内容安全,且确实需要完整功能时,才考虑StandardEvaluationContext
  • 红线三:定期对项目依赖的Spring框架版本进行升级。Spring团队会持续修复安全漏洞,保持使用较新的稳定版能避免已知的SpEL相关漏洞。

5. 常见问题排查与防御加固技巧

在实际开发和审计中,总会遇到一些模糊地带和疑难杂症。这里分享一些我踩过坑后总结的经验。

5.1 问题排查清单

当你怀疑或确认一个SpEL注入点时,可以按此清单进行深入排查:

问题排查点工具/方法
数据流不清晰输入参数经过多层传递、封装,难以追踪。1. IDE调试模式,跟踪变量。
2. 在可能的关键方法入口打日志,打印参数值。
3. 使用代码分析工具(如IntelliJ的“Find Usages”)追溯调用链。
表达式是否真的执行了?某些条件下表达式可能不被解析(如@Cacheableunless条件为true)。1. 在parseExpressiongetValue处打断点。
2. 使用无害的验证payload(如T(java.lang.System).currentTimeMillis())并观察日志或返回值。
漏洞是否可稳定利用?Payload在某些环境下失败(如Linux/Windows命令差异,Java安全管理器限制)。1. 尝试使用与目标环境兼容的Payload(如Linux用/bin/sh -c,Windows用cmd /c)。
2. 尝试使用纯Java反射的Payload绕过可能的命令过滤:#{T(org.springframework.util.StreamUtils).copy(T(java.lang.Runtime).getRuntime().exec('whoami').getInputStream(), T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())}(此Payload尝试将命令结果写入HTTP响应,极度危险,仅用于授权测试理解原理)。
修复是否引入新问题?StandardEvaluationContext改为SimpleEvaluationContext后,原有合法功能报错。1. 全面回归测试依赖SpEL功能的业务场景。
2. 如果确实需要部分方法调用,使用SimpleEvaluationContext.Builder谨慎配置允许的访问范围,但这会重新引入风险,需极其慎重。

5.2 防御加固技巧

除了修复具体漏洞,还可以在架构和流程层面进行加固:

  1. 安全组件封装:如果项目中有多处需要使用动态SpEL,建议封装一个安全的SpEL工具类。这个工具类内部强制使用SimpleEvaluationContext,并提供日志记录、表达式复杂度限制等功能。

    @Component public class SafeSpelEvaluator { private final ExpressionParser parser = new SpelExpressionParser(); private final SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); private static final Set<Class<?>> ALLOWED_RETURN_TYPES = Set.of(String.class, Number.class, Boolean.class); public <T> T evaluate(String expression, Class<T> returnType) { if (!ALLOWED_RETURN_TYPES.contains(returnType)) { throw new IllegalArgumentException("Unsupported return type for safe evaluation"); } try { // 可在此添加表达式长度、复杂度检查 if (expression.length() > 100) { // 示例限制 throw new IllegalArgumentException("Expression too long"); } return parser.parseExpression(expression).getValue(context, returnType); } catch (Exception e) { log.warn("Safe SpEL evaluation failed for expression: {}", expression, e); throw new RuntimeException("Expression evaluation error", e); } } }
  2. 代码审计纳入CI/CD:将SpotBugs with Find Security Bugs这类静态扫描工具集成到持续集成流水线中。设置门禁,如果扫描出高危的“SPEL_INJECTION”问题,则阻断合并请求,从流程上杜绝漏洞引入。

  3. 依赖库版本管理:使用Maven的versions:display-dependency-updates或Gradle的dependencyUpdates插件定期检查依赖更新。确保Spring Framework及相关组件(Spring Security, Spring Data等)保持最新稳定版本,及时获取安全补丁。

  4. 安全培训与案例分享:在开发团队内部定期进行安全编码培训,将SpEL注入作为一个典型案例进行讲解。分享内部审计发现的实际案例(脱敏后),能让开发人员对这类风险有更直观和深刻的认识,从而在编码时主动规避。

SpEL表达式注入的审计,归根结底是对“数据流”和“信任边界”的审视。它要求安全人员和开发人员不仅要知道怎么用框架,更要理解框架背后的运行机制和潜在的风险假设。养成对任何“将外部数据作为代码执行”的行为保持条件反射般的警惕,是构建安全软件系统的重要心智模型。每次看到parseExpression这个词,不妨在心里多问一句:这个表达式,我真的能完全信任它吗?

http://www.gsyq.cn/news/1569090.html

相关文章:

  • LangGraph+Gradio实战:构建可调试可扩展的Agent系统
  • BGU8053低噪声放大器设计:噪声系数与线性度平衡实战
  • 深圳搬家哪家强?2026年实测5家口碑公司,从起步价到附加费全拆解,拒绝坐地起价 - 从来都是英雄出少年
  • 003-费曼独立思考的底层哲学
  • 本地Codex搭建实战:Ollama+Continue分层部署指南
  • 两个小物件儿 ☜请点击这里可看全文
  • 2026无锡本土靠谱GEO SEO优化公司3家本土服务商实测:实测避坑,企业AI获客少走弯路 - wxxwlm
  • 2026锦州本地正规瓷砖空鼓维修服务商盘点|无损免拆砖修复,全域上门售后有保障 - 宅安选房屋修缮
  • 嵌入式Linux从NFS迁移到本地硬盘启动:MPC8220平台移植实战
  • 3分钟上手!B站会员购抢票神器:免费自动化购票终极指南
  • 2026年官方详解:合肥理工学校招生简章 - hflgzz
  • Java面向对象程序设计——4~6次作业集总结
  • Sunshine游戏串流终极指南:跨平台兼容性与零延迟实战技巧
  • 英雄联盟玩家的专业效率工具:League Akari 完整使用指南
  • 终极智能分层工具:5分钟掌握LayerDivider插画自动分层技巧
  • 终极B站会员购抢票指南:用biliTickerBuy轻松搞定限量商品
  • C++ 核心面向对象:类与对象超全精讲|封装、成员属性、权限、新手避坑
  • 魔兽争霸3终极优化指南:5个简单技巧让经典游戏在现代电脑上流畅运行
  • 2026年河源龙川黄金回收店铺实地探访,核心推荐龙川源奢汇及正规门店选择指南 - 行走在冷风中。
  • Ubuntu 12.04下Resilio Sync(原BTSync)本地去中心化同步实战
  • 基于LIN总线的车窗控制:MM908E624软件架构与防夹算法详解
  • 基于事件驱动的自动化游戏辅助系统:D3KeyHelper技术架构深度解析
  • 从MMC2114到MCF5282:ColdFire MCU迁移实战与性能优化指南
  • 炉石传说智能对战脚本:5步轻松实现自动化对战
  • 基于享乐博弈论的LLM多智能体联盟稳定性分析与CoalT协议实践
  • 如何搭建高性能游戏串流服务器:Sunshine配置与优化实战指南
  • iOS虚拟定位新选择:iFakeLocation的实用指南
  • PowerQUICC II PCI桥接器DMA传输与中断同步实战解析
  • Mac NTFS硬盘读写终极指南:免费开源方案解决跨平台文件传输难题
  • 兰州买猫买狗哪家靠谱?5家正规猫犬舍实测,皇克莱榜首 - 同城宠物优选基地