SpEL表达式注入漏洞:原理、挖掘与防御实战 📅 2026/6/20 14:00:49 1. 项目概述当表达式成为攻击者的利刃在Java应用安全的世界里代码审计就像一场没有硝烟的攻防战。我们作为防御方每天要面对无数潜在的“暗箭”而其中一种极具迷惑性和破坏性的武器就是Spring Expression Language简称SpEL。你可能在Spring框架的Value注解、Thymeleaf模板或者Spring Security的权限控制里都见过它的身影。它强大、灵活能让开发者用简洁的表达式完成复杂的逻辑。但正是这份强大一旦被恶意利用就会瞬间从开发利器变成攻击者手中的“利刃”直接刺穿应用的核心。我处理过不少安全事件其中由SpEL表达式注入引发的漏洞往往让人印象深刻。攻击者不需要知道你的数据库结构也不需要绕过复杂的身份验证他们只需要找到一个能控制表达式内容的地方就能让服务器执行任意代码轻则信息泄露重则服务器被完全控制。这种漏洞的隐蔽性在于它看起来可能只是一个普通的配置项或者一个模板渲染的参数审计时稍不留神就会滑过去。今天我们就来彻底拆解这把“利刃”不仅要知道它如何伤人更要学会如何锻造我们的“盾牌”在代码审计中精准识别并有效防御SpEL表达式漏洞。无论你是刚入门的安全工程师还是正在备战Java面试的开发者理解这个主题都至关重要它直接关系到你手中系统的“内功”是否扎实。2. SpEL表达式漏洞的核心原理与攻击面解析要防御必须先透彻理解攻击是如何发生的。SpEL漏洞的本质是“将不可信的数据当作代码执行”这听起来和SQL注入、OS命令注入如出一辙都属于“注入”类漏洞的大家族。但SpEL的独特之处在于它的执行上下文和强大的功能集。2.1 SpEL的“力量之源”与危险边界Spring Expression Language并非一个独立的脚本引擎而是深度集成在Spring容器中的表达式解析器。它的设计目标是提供一个强大的、用于在运行时查询和操作对象图的统一表达式语言。其核心能力包括属性访问与方法调用如user.name、user.activate()。类型操作使用T()操作符调用静态方法和常量如T(java.lang.Runtime)。集合操作与投影支持对集合进行筛选、映射等操作。运算符与正则表达式支持算术、逻辑、关系运算及正则匹配。变量定义与引用可以通过#variableName引用上下文中的变量。安全问题的导火索就在于T()操作符和new操作符。在默认的、未经过安全加固的StandardEvaluationContext上下文中SpEL允许表达式调用任何类的静态方法或构造器。这就为攻击者打开了一扇通往java.lang.Runtime或java.lang.ProcessBuilder的大门。注意很多开发者会混淆StandardEvaluationContext和SimpleEvaluationContext。前者功能完整但危险后者是Spring后期为了安全而引入的、功能受限的上下文默认不支持类型操作T()和bean引用是防御的第一道关口。在审计时看到代码中使用StandardEvaluationContext解析用户输入就需要立即拉起警报。2.2 典型攻击向量与漏洞入口点在代码审计中我们需要像攻击者一样思考寻找那些用户输入可能“流”入SpEL解析器的地方。以下是几个高危的入口点Spring MVC 参数绑定与注解Value注解这是最经典的案例。如果应用从外部配置如环境变量、配置文件动态获取Value的值而这个外部配置源如某个HTTP请求头可被攻击者控制就可能引发问题。例如Value(“${user.input}”)如果user.input来自请求参数且内容为#{T(java.lang.Runtime).getRuntime().exec(‘calc’)}在解析时就会触发命令执行。请求参数映射到SpEL表达式某些自定义的解析器或框架可能直接将请求参数值传递给SpelExpressionParser进行解析。Spring Security 表达式在配置URL访问权限时如hasAuthority(‘ADMIN’)是安全的但如果权限表达式的一部分来自用户可控的数据例如从数据库读取的角色权限规则且使用了StandardEvaluationContext则存在风险。模板引擎的表达式解析虽然Thymeleaf本身对表达式有沙箱限制但如果在集成或自定义扩展时处理不当用户输入仍可能进入SpEL解析流程。审计时需要关注模板中动态渲染的部分尤其是那些使用th:text”${…}”且内容部分可控的场景。自定义的表达式解析服务这是最隐蔽也最危险的一类。业务中可能需要实现动态规则引擎、计算器等功能开发者自己编写了调用SpelExpressionParser.parseExpression()的代码。如果解析的表达式字符串直接或间接拼接了用户输入漏洞就产生了。// 一个危险的自定义解析器示例 public Object evaluateUserExpression(String userInput) { ExpressionParser parser new SpelExpressionParser(); // 致命错误使用StandardEvaluationContext且直接解析用户输入 StandardEvaluationContext context new StandardEvaluationContext(); Expression exp parser.parseExpression(userInput); // userInput可能为恶意表达式 return exp.getValue(context); }实操心得在审计时全局搜索SpelExpressionParser、StandardEvaluationContext、parseExpression、Value等关键词是第一步。但更重要的是进行数据流追踪确认传入这些解析器的字符串其源头是否最终可被外部用户控制。一个可控的源头加上一个危险的解析上下文就构成了一个完整的漏洞链。3. 漏洞挖掘手工与工具结合的审计实战知道了原理和入口接下来就是如何在浩如烟海的代码中找到它们。我习惯采用“自上而下”和“自下而上”相结合的策略。3.1 静态代码审计定位可疑代码模式静态分析是代码审计的基石。我们可以借助IDE的搜索功能和专门的静态应用安全测试SAST工具。关键词搜索与模式识别高风险类/方法搜索org.springframework.expression.spel.standard.SpelExpressionParser、org.springframework.expression.spel.support.StandardEvaluationContext。解析方法调用搜索.parseExpression(查看其参数来源。注解扫描搜索Value特别是其值使用了${…}或#{…}格式的。需要审查这些占位符对应的属性源如Environment的加载逻辑看是否有从请求参数、头、Cookie等不可信源加载的配置。数据流分析手动 这是最考验功力的部分。当你找到一个parseExpression(userInput)调用时需要逆向追踪userInput这个变量的来源。它可能来自HttpServletRequest.getParameter()。可能来自数据库查询结果而数据库的数据最初又可能来自用户输入。可能来自反序列化后的对象属性。可能经过多层服务传递和字符串拼接。你需要像侦探一样沿着方法调用的链条向上回溯绘制出数据从“污染源”Source到“危险函数”Sink的完整路径。使用SAST工具辅助 工具如SonarQube、Fortify SCA、Checkmarx都有内置的规则来检测潜在的SpEL注入。它们能自动化地进行一部分数据流分析快速定位大量可疑点。但切记工具报告的是“潜在”漏洞存在误报将安全代码报为漏洞和漏报未能发现真正的漏洞。审计员的职责就是对这些结果进行人工验证和深度分析。3.2 动态测试与漏洞验证静态分析找到疑点后必须通过动态测试来验证漏洞是否真实存在、是否可利用。构造验证Payload 对于SpEL注入一个安全的验证Payload至关重要。我们不应该直接使用Runtime.exec(“calc”)或rm -rf这样的危险命令。延迟测试利用T(java.lang.Thread).sleep(5000)。如果服务器响应有明显延迟说明表达式被执行了。DNS外带测试利用T(java.net.InetAddress).getByName(‘attacker-controlled-domain.com’)。在你的DNS日志中查看是否有解析请求这是证明漏洞存在且可触发网络交互的铁证。文件读取测试谨慎在授权测试范围内尝试new java.io.FileInputStream(‘/etc/passwd’).read()的变体但需注意可能触发内部异常需结合响应差异判断。// 示例一个用于验证的、相对无害的Payload String testPayload “#{T(java.lang.Thread).sleep(3000)}”; // 睡眠3秒 // 或 String dnsPayload “#{T(java.net.InetAddress).getByName(‘subdomain.yourcollaborator.net’)}”;测试点注入 根据静态分析找到的入口将Payload注入到相应的参数中。这可能通过HTTP请求参数GET /api/calc?expression恶意PayloadHTTP请求头X-Config-Value: 恶意PayloadPOST BodyJSON/XML/表单。Cookie值。 使用Burp Suite、Postman等工具发送这些精心构造的请求并观察服务器的响应时间、响应内容、以及你的DNS/HTTP监听服务如Burp Collaborator是否有回调。上下文绕过技巧探索 有时输入点可能被包裹在特定上下文中如#{‘userInput’}。你需要尝试闭合原有的表达式。例如如果代码拼接方式为”#{‘” userInput “‘}”你可以输入’} T(java.lang.Runtime).getRuntime().exec(‘calc’) #{‘使得最终解析的表达式变为#{’’} T(java.lang.Runtime).getRuntime().exec(‘calc’) #{’’}从而执行恶意代码。这需要对代码的拼接逻辑有清晰的猜测。常见问题实录在一次审计中我发现一个Value(“${report.template}”)注解report.template来自一个配置中心。静态分析认为配置中心是可信的。但动态测试时我发现该配置中心的API接口存在未授权访问漏洞可以任意修改配置值。这就将SpEL漏洞的入口从应用本身转移到了配置管理系统攻击面被扩大了。这提醒我们审计时不能只看代码本身还要考虑与之交互的上下游系统是否安全。4. 纵深防御从编码到配置的全面加固找到并修复漏洞是目标但构建一个不易被攻破的体系才是终极追求。针对SpEL表达式漏洞我们需要建立多层次的防御。4.1 编码层防御使用安全的API与上下文这是最根本、最有效的防御措施。强制使用SimpleEvaluationContext 对于所有需要解析不可信或外部输入的场景必须使用SimpleEvaluationContext。它通过构造器指定可访问的属性范围从根本上禁用了危险的T()和new操作符以及bean引用。// 安全的写法 ExpressionParser parser new SpelExpressionParser(); // 创建SimpleEvaluationContext并限制只能访问‘rootObject’的属性 EvaluationContext context SimpleEvaluationContext.forReadOnlyDataBinding() .withRootObject(rootObject) .build(); // 尝试解析恶意表达式将抛出异常EL1008E: Property or field ‘java’ cannot be found on object of type ‘com.example.MyRootObject’ Object result parser.parseExpression(userInput).getValue(context);输入校验与白名单 如果业务必须使用StandardEvaluationContext例如需要调用静态方法实现特定功能那么必须对输入进行严格的校验。白名单校验定义允许的表达式模式。例如如果只允许进行简单的数学计算可以使用正则表达式严格匹配如^[0-9\\-*/().\\s]$。语法树分析在解析前先使用SpEL的SpelExpression对象获取表达式的抽象语法树AST遍历树节点检查是否包含TypeReference对应T()、ConstructorReference对应new等危险节点一旦发现立即拒绝。避免字符串拼接 绝对不要用字符串拼接的方式构建表达式。应采用参数化构造利用SpEL本身的变量绑定功能。// 危险拼接 String dangerousExpr “user.” userControlledProperty “ ‘admin”; // 安全参数化 String safeExpr “user[?(.” placeholder “ ‘admin’)]”; // 仍需要校验placeholder // 或更优使用变量绑定 Expression expr parser.parseExpression(“property expectedValue”); context.setVariable(“property”, userControlledPropertyName); // 变量值非表达式部分 context.setVariable(“expectedValue”, “admin”);4.2 架构与配置层防御沙箱环境隔离 对于必须执行动态表达式的核心业务如规则引擎可以考虑在独立的、受限制的沙箱环境中运行。例如使用Java安全管理器SecurityManager配置严格的策略文件或将其部署到权限极低的独立容器/进程中即使被突破影响范围也有限。依赖库与版本管理 确保使用的Spring框架及相关库如Spring Security是最新的稳定版本。历史版本中的SpEL相关漏洞如CVE-2022-22965 Spring4Shell的部分利用链涉及SpEL已被修复。定期更新依赖是成本最低的防御手段之一。安全配置审查审查Spring Boot的application.properties/yml确保没有将敏感或不可信的配置源如某个URL以高优先级加载到Environment中。在Spring Security配置中检查所有使用PreAuthorize、PostAuthorize或XML配置中的intercept-url表达式确保其硬编码或来自绝对可信的来源。4.3 运行时监控与应急响应防御不可能100%完美因此需要监控作为最后一道防线。日志监控 在SpEL解析器周围添加详细的WARN或ERROR级别日志记录解析失败的表达式及其来源。频繁出现畸形或包含可疑关键词如Runtime、ProcessBuilder、getClassLoader的表达式解析失败日志可能是攻击者正在探测的信号。RASP运行时应用自我保护 部署RASP探针。RASP可以在应用内部监控关键函数如SpelExpressionParser.parseExpression、MethodInvoker.invoke的调用栈和参数。当检测到试图通过SpEL调用危险方法如Runtime.exec的行为时可以实时阻断请求并告警。这是一种非常有效的动态防御技术。WAF规则 在Web应用防火墙WAF上部署针对SpEL注入特征的规则例如检测请求参数中是否包含T(、#{、new等典型模式。但要注意这只能防御“懒”的攻击者对于编码绕过或非常规利用方式可能失效不能替代代码层的安全修复。个人体会防御SpEL漏洞技术手段固然重要但更重要的是将安全思维融入开发流程。在代码评审Code Review环节将“SpEL解析用户输入”作为必须检查的高危项在安全培训中向开发团队普及SimpleEvaluationContext和StandardEvaluationContext的区别。我曾推动团队将“禁止使用StandardEvaluationContext解析任何外部输入”写进了编码规范并在CI/CD流水线中通过静态扫描工具卡点从源头大幅减少了此类漏洞的引入。真正的安全是让正确的做法成为习惯。