注解中的 SpEL 表达式执行原理与注入风险详解

注解中的 SpEL 表达式执行原理与注入风险详解

🚨注解中的 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);

✅ 执行过程:

  1. Spring 容器初始化时,扫描到 @Value
  2. 判断字符串是否以 #{...} 包裹;
  3. 使用 SpelExpressionParser 解析表达式;
  4. 构造 StandardEvaluationContext,注入 Bean;
  5. 执行表达式 → 把返回值注入字段。

🔍 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);

✅ 执行过程:

  1. Spring Security 初始化时扫描 @PreAuthorize
  2. ExpressionHandler(默认 DefaultMethodSecurityExpressionHandler)解析表达式;
  3. 自动注入 #id 为方法参数;
  4. 创建 EvaluationContext 并设置 Bean;
  5. 执行表达式 userService.canAccess(#id) 判断权限;
  6. 若返回 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
喜欢就支持一下吧
点赞5赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容