🚨注解中的 SpEL 表达式执行原理与注入风险详解
—— 从 @Value
到 @PreAuthorize
,揭开注解里的动态表达式黑盒
🧩 一、概述
Spring 中很多注解都支持 SpEL 表达式(Spring Expression Language),比如:
@Value("#{...}")
– 给字段注入表达式计算值;@PreAuthorize("#{...}")
– 动态控制访问权限;@Scheduled("#{...}")
– 动态定时任务表达式;@Cacheable(value = "#{...}")
– 动态缓存键。
这些看似简单的注解表达式,本质上背后都在动态执行 SpEL,一旦表达式内容被用户控制,就可能造成 SpEL 注入。
🎯 二、SpEL 如何在注解中被执行?
我们以 @Value
和 @PreAuthorize
为例,详细拆解执行过程。
🔍 1.@Value("#{...}")
注入流程
@Value("#{userService.getWelcomeMessage()}")
private String message;
其中 @Value("#{userService.getWelcomeMessage()}")
底层等价于:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext evalContext = new StandardEvaluationContext();
// 注入 Spring 中的 Bean 容器
((StandardEvaluationContext) evalContext).setBeanResolver(new BeanFactoryResolver(context));
// 执行 SpEL 表达式
Expression expr = parser.parseExpression("@userService.getWelcomeMessage()");
String result = expr.getValue(evalContext, String.class);
✅ 执行过程:
- Spring 容器初始化时,扫描到
@Value
; - 判断字符串是否以
#{...}
包裹; - 使用
SpelExpressionParser
解析表达式; - 构造
StandardEvaluationContext
,注入 Bean; - 执行表达式 → 把返回值注入字段。
🔍 2. @PreAuthorize("#{...}")
权限控制
@PreAuthorize("#{userService.canAccess(#id)}")
@GetMapping("/data/{id}")
public String getData(@PathVariable Long id) {
return "secure content";
}
其中 @PreAuthorize("#{userService.canAccess(#id)}")
等价于:
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("id","www.geekserver.top");
context.setRootObject(new CustomMethodSecurityExpressionRoot(auth));
context.setBeanResolver(new BeanFactoryResolver(applicationContext)); // 解析 userService
// 解析表达式
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("userService.canAccess(#id)");
// 执行表达式
Boolean access = exp.getValue(context, Boolean.class);
✅ 执行过程:
- Spring Security 初始化时扫描
@PreAuthorize
; - 用
ExpressionHandler
(默认DefaultMethodSecurityExpressionHandler
)解析表达式; - 自动注入
#id
为方法参数; - 创建
EvaluationContext
并设置 Bean; - 执行表达式
userService.canAccess(#id)
判断权限; - 若返回
true
才允许访问方法。
🚫 三、SPEL 注入风险分析
❌ 示例一:拼接表达式
@Value("#{user.id = ${user.input}}") // 用户可控配置 = SpEL 注入入口
private String unsafe;
输入:
user.input=T(java.lang.Runtime).getRuntime().exec('calc')
💣 Spring 会执行表达式,等价于:
Runtime.getRuntime().exec("calc");
❌ 示例二:动态表达式
@PreAuthorize("#{T(com.example.SpelHelper).getDynamicExpr()}")
SpelHelper.java
:
public class SpelHelper {
public static String getDynamicExpr() {
// ❌ 不安全:如果这部分用户可控,可以返回下面内容
return "T(java.lang.Runtime).getRuntime().exec('notepad')";
}
}
💥 执行时,Spring 会将返回的字符串再次解析为 SpEL 表达式并执行,造成远程命令执行。
总结:如果表达式是由用户输入、数据库读取、配置中动态生成的,那都增加了注入风险,一旦校验不严格就会RCE —— 非常危险!
🧾 四、总结
一句话总结:表达式不能拼接,不能来源于外部可控输入
项目 | 分析 |
---|---|
@Value("${...}") | ✅读取配置,不执行 SpEL |
@Value("#{...}") | ⚠️执行表达式,要避免拼接 |
@PreAuthorize("#{...}") | ⚠️固定写法安全,拼接/外部表达式不安全 |
动态 SpEL 来源用户/配置/数据库 | ❌极易被注入执行恶意表达式 |
使用 setVariable() 注入用户值 | ✅注入值作为变量字符串,不参与表达式结构组成 |
使用 SimpleEvaluationContext | ✅严格限制表达式能力,建议用于用户参与的 SpEL |
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容