news 2026/1/29 23:13:44

spring中el表达式安全和扩展

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
spring中el表达式安全和扩展

0. 背景

Spring的核心技术SpEL底层采用反射的方式获取对象属性、调用方法、创建对象等。如果不加以限制有非常大的安全漏洞。
如果访问权限过大,系统接收的字符串,很容易就执行恶意程序.比如在上一章 Spring使用el表达式 第一小节中执行的表达式T(Runtime).getRuntime().exec('calc')就轻松运行了windows的计算器。

Spring 默认提供了两个context

  1. StandardEvaluationContext: 默认的context,可以访问任意对象属性、调用任意对象方法、创建任意对象
  2. SimpleEvaluationContext: 功能首先的上下文,可以快速限制部分能力,要想安全控制并且想省事,可以直接使用这个.

以下章节依次说明可以限制的地方以及实现方法.从开发角度来说可以是限制,也可以说是扩展

1. 限制类

在引擎加载具体字节码时,可以通过自定义TypeLocator限制某些类的加载.比如以下代码,实现一个白名单功能,为了针对性的处理,这个类直接从StandardTypeLocator扩展.然后重写findType方法

public class LimtClassTypeLocator extends StandardTypeLocator { Set<String> whiteClassSet = new HashSet<>(); public LimtClassTypeLocator(String... className) { whiteClassSet = new HashSet<>(Arrays.asList(className)); } @Override public Class<?> findType(String typeName) throws EvaluationException { if (this.whiteClassSet.contains(typeName)) { return super.findType(typeName); } throw new EvaluationException("类名: " + typeName + "不允许调用"); } }

然后我们需依次测试一下几种情况

  • 使用T(ClassName)语法测试白名单之内和之外的class
  • 使用new ClassName()语法测试白名单之内和之外的class
  • 测试inline list语法 '{1,2,3}'
  • 测试inline map语法 '{1:'a',2:'b'}'

测试代码中可以访问的class设置为org.apache.commons.lang.StringUtils,测试代码如下

public void testLimtClass() { ExpressionParser elParser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setTypeLocator(new LimtClassTypeLocator("org.apache.commons.lang.StringUtils")); System.out.println("===TypeLocator: 测试白名单的class==="); String whiteExpr = "T(org.apache.commons.lang.StringUtils).substring('小游戏 地心侠士',4)"; Object whiteValue = elParser.parseExpression(whiteExpr).getValue(ctx); System.out.println(whiteValue); System.out.println("===TypeLocator: 测试非法的class==="); try { String illegalClass = "T(org.apache.commons.lang.StringEscapeUtils).escapeHtml('小游戏 地心侠士')"; Object illegalValue = elParser.parseExpression(illegalClass).getValue(ctx); } catch (EvaluationException e) { System.out.println(e.getMessage()); } System.out.println("===TypeLocator: 测试非法构造函数=="); String cotrExpr = "new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss')"; try { SimpleDateFormat df = (SimpleDateFormat) elParser.parseExpression(cotrExpr).getValue(ctx); System.out.println("当前时间: " + df.format(new Date())); } catch (EvaluationException e) { System.out.println(e.getMessage()); } System.out.println("===TypeLocator: 测试拦截内联list==="); String initList = "{'小游戏','地心侠士'}"; // java.util.Collections$UnmodifiableRandomAccessList<?> List inlineLst = elParser.parseExpression(initList).getValue(ctx, List.class); inlineLst.forEach(System.out::println); System.out.println("===TypeLocator: 测试拦截内联Map==="); //java.util.Collections$UnmodifiableMap<?, ?> String initMap = "{'gameType':'小游戏','gameName':'地心侠士'}"; Map inlineMap = elParser.parseExpression(initMap).getValue(ctx, Map.class); inlineMap.forEach((k, v) -> System.out.println(k + " : " + v)); }

测试允许结果如下

===TypeLocator: 测试白名单的class=== 地心侠士 ===TypeLocator: 测试非法的class=== 类名: org.apache.commons.lang.StringEscapeUtils不允许调用 ===TypeLocator: 测试非法构造函数== EL1003E: A problem occurred whilst attempting to construct an object of type 'java.text.SimpleDateFormat' using arguments '(java.lang.String)' ===TypeLocator: 测试拦截内联list=== 小游戏 地心侠士 ===TypeLocator: 测试拦截内联Map=== gameType : 小游戏 gameName : 地心侠士

从测试结果可以看LimtClassTypeLocator有效拦截了非法的静态方法调用以及非法的构造函数调用.但其中有一点,内联的list和map 拦截失败了

关键代码:ctx.setTypeLocator(new LimtClassTypeLocator("org.apache.commons.lang.StringUtils"))

2. 限制属性

spel引擎中,可以直接对对象属性进行读写操作.要限制属性读和写,需要通过实现PropertyAccessor接口.该接口提供4个方法,依次是两个读写判断,以及读写操作.
如果需要对属性值特殊处理,也可以通过此能力实现.比如把电话号中间几位改成星号.LimitPropertyAccessors这个类,不允许读的属性为gameType,
不允许写的属性为gameName.具体代码如下

public class LimitPropertyAccessors extends ReflectivePropertyAccessor { @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if ("gameType".equals(name)) { throw new AccessException("gameType属性不允许访问"); } return super.canRead(context, target, name); } @Override public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if ("gameName".equals(name)) { throw new AccessException("gameName 属性不允许赋值"); } return super.canWrite(context, target, name); } }

属性读测试代码如下:

public void testLimitReadProperty() { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors())); ExpressionParser elParser = new SpelExpressionParser(); ctx.setVariable("var", testObject); String allowProp = "#var.gameName"; System.out.println("===测试允许访问的属性 gameName==="); Object value = elParser.parseExpression(allowProp).getValue(ctx); System.out.println("获取成功: " + value); String disallowProp = "#var.gameType"; System.out.println("===测试禁止访问的属性 gameType ==="); try { value = elParser.parseExpression(disallowProp).getValue(ctx); System.out.println(value); } catch (Exception e) { System.out.println("属性访问失败: " + e.getMessage()); } }

运行结果如下:

===测试允许访问的属性 gameName=== 获取成功: 地心侠士 ===测试禁止访问的属性 gameType === 属性访问失败: EL1021E: A problem occurred whilst attempting to access the property 'gameType': 'gameType属性不允许访问'

属性写测试代码如下:

public void testLimitWriteProperty() { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); System.out.println("===对象原始值==="); System.out.println(testObject.toString()); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors())); ExpressionParser elParser = new SpelExpressionParser(); ctx.setVariable("var", testObject); String allowWriteProp = "#var.gameType='小游戏 666'"; elParser.parseExpression(allowWriteProp).getValue(ctx); System.out.println("===测试可以赋值属性 gameType ==="); System.out.println(testObject.toString()); System.out.println("===测试不可以访问属性 gameName ==="); String disAllowWriteProp = "#var.gameName='地心侠士 666'"; try { elParser.parseExpression(disAllowWriteProp).getValue(ctx); } catch (Exception e) { System.out.println("属性赋值失败: " + e.getMessage()); } }

运行结果如下

===对象原始值=== ElTestObject [gameType=小游戏, gameName=地心侠士] ===测试可以赋值属性 gameType === ElTestObject [gameType=小游戏 666, gameName=地心侠士] ===测试不可以访问属性 gameName === 属性赋值失败: EL1034E: A problem occurred whilst attempting to set the property 'gameName': gameName 属性不允许赋值

关键代码:ctx.setPropertyAccessors(Arrays.asList(new LimitPropertyAccessors()));

3. 限制方法

限制某些方法不能调用,需要实现MethodFilter接口.通过filter方法返回可以调用的方法.该接口定义为@FunctionalInterface可以直接使用lambda表达式实现.

测试代码如下:

public void testLimitMethod() { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); StandardEvaluationContext ctx = new StandardEvaluationContext(); // 设置成只能调用 setGameName 方法 ctx.registerMethodFilter(ElTestObject.class, method -> { return method.stream().filter(m -> m.getName().equals("getGameName")).toList(); }); ctx.setVariable("var", testObject); String limitMethod = "#var.getGameType()"; System.out.println("===调用getGameType==="); ExpressionParser elParser = new SpelExpressionParser(); try { Object limitValue = elParser.parseExpression(limitMethod).getValue(ctx); System.out.println(limitValue); } catch (Exception e) { System.out.println("调用getGameType失败:" + e.getMessage()); } System.out.println("===调用getGameName==="); String allowMethod = "#var.getGameName()"; Object allowValue = elParser.parseExpression(allowMethod).getValue(ctx); System.out.println("获取成功: " + allowValue); }

测试结果如下

===调用getGameType=== 调用getGameType失败:EL1004E: Method call: Method getGameType() cannot be found on type com.herbert.script.SpringElScriptSafeAndExtend$ElTestObject ===调用getGameName=== 获取成功: 地心侠士

关键代码:ctx.registerMethodFilter(ElTestObject.class, method->method)

4. 限制Bean

引擎使用@beanName语法,可以访问对应bean,针对一些特殊存在的bean,可以限制使用,这里需要实现接口BeanResolver
自定义一个BeanResolver.
代码如下:

public class LimtBeanResolver implements BeanResolver { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); @Override public Object resolve(EvaluationContext context, String beanName) throws AccessException { if ("game".equals(beanName)) { return testObject; } return new String("不允许访问的bean:[" + beanName + "]"); } }

从代码可知,如果传递的@game会返回实例testObject,其他则返回"不允许访问的bean:[" + beanName + "]"
测试代码如下

public void limitBean() { ExpressionParser elParser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); // ctx.setBeanResolver(new BeanFactoryResolver((BeanFactory) applicationContext)); ctx.setBeanResolver(new LimtBeanResolver()); Object value = elParser.parseExpression("@game").getValue(ctx); System.out.println("===测试允许访问的bean==="); System.out.println(value); value = elParser.parseExpression("@other").getValue(ctx); System.out.println("===测试不允许访问的bean==="); System.out.println(value); }

运行结果如下

===测试允许访问的bean=== ElTestObject [gameType=小游戏, gameName=地心侠士] ===测试不允许访问的bean=== 不允许访问的bean:[other]

关键代码:ctx.setBeanResolver(new LimtBeanResolver());

5. 限制内容

有时需要对应脚本中的参数内容做一些特殊处理,这时就需要通过TypeConverter对一些值做一些特殊处理.除此之外还可以通过PropertyAccessors实现.接下来,我们实现一个TypeConverter,主要功能是把参数中的666替换成999,代码如下

public class LimtTypeConvert extends StandardTypeConverter { @Override public @Nullable Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (value.equals(666)) { value = 999; } return super.convertValue(value, sourceType, targetType); } }

测试代码如下

public void testLimtValue() { String initList = "'小游戏 地心侠士' + 666 "; ExpressionParser elParser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setTypeConverter(new LimtTypeConvert()); Object expValue = elParser.parseExpression(initList).getValue(ctx); System.out.println("===使用typeconvert,将666变成999==="); System.out.println(expValue); }

运行结果如下:

===使用typeconvert,将666变成999=== 小游戏 地心侠士999

关键代码:ctx.setTypeConverter(new LimtTypeConvert());

6. 扩展函数

引擎中的函数扩展,实际就是把函数作为一个变量放到ctx中,然后通过访问对象的方式调用该函数.测试代码如下

public void testExtendFunction() throws NoSuchMethodException, SecurityException { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); ExpressionParser elParser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); Method method = ElTestObject.class.getMethod("joinString", String.class, String.class); // 内部调用 setVariable ctx.registerFunction("joinString", method); ctx.setVariable("var", testObject); ctx.setVariable("method", method); String extendMethod = "#joinString(#var.gameType,#var.gameName)"; System.out.println("===测试扩展方法(registerFunction)==="); Object value = elParser.parseExpression(extendMethod).getValue(ctx); System.out.println(value); extendMethod = "#method(#var.gameType,#var.gameName)"; System.out.println("===测试扩展方法(setVariable)"); value = elParser.parseExpression(extendMethod).getValue(ctx); System.out.println(value); }

运行结果如下:

===测试扩展方法(registerFunction)=== 注册方法调用成功: 小游戏 : 地心侠士 ===测试扩展方法(setVariable) 注册方法调用成功: 小游戏 : 地心侠士

关键代码:ctx.registerFunction("joinString", method);

7. 操作符重写

操作符重写,只能重写部分数字相关的操作.并且操作符两边,至少有一边是数字才行.需要是想接口OperatorOverloader,我们这实现一个对象+数字的功能

public class AddExtendStringToObj implements OperatorOverloader { @Override public boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) throws EvaluationException { if (operation == Operation.ADD && leftOperand instanceof ElTestObject && NumberUtils.isNumber(rightOperand.toString())) { return true; } return false; } @Override public Object operate(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) throws EvaluationException { ElTestObject left = (ElTestObject) leftOperand; left.setGameName(left.getGameName() + rightOperand.toString()); return leftOperand; } }

测试代码如下:

public void testExtendOperator() { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); ExpressionParser elParser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setOperatorOverloader(new AddExtendStringToObj()); ctx.setVariable("var", testObject); String el = "#var + 666"; Object value = elParser.parseExpression(el).getValue(ctx); System.out.println("===测试操作符重写==="); System.out.println(value); }

运行结果如下:

===测试操作符重写=== ElTestObject [gameType=小游戏, gameName=地心侠士666]

从测试结果可以看出 666 的数字被添加到对象gameName中.
关键代码:ctx.setOperatorOverloader(new AddExtendStringToObj());

8. SimpleEvaluationContext 使用

SimpleEvaluationContext是一个构造模式的上下文,需要使用build构造具体功能的上下文.
快速实现一个只读的上下文测试代码如下:

public void testOnlyRead() { ElTestObject testObject = new ElTestObject("小游戏", "地心侠士"); // 不会主动注册 MethodResolver 不能访问方法 SimpleEvaluationContext safeContxt = SimpleEvaluationContext.forReadOnlyDataBinding().build(); safeContxt = SimpleEvaluationContext.forReadOnlyDataBinding(). build(); safeContxt.setVariable("var", testObject); System.out.println("===测试只读模式 读取值==="); String readExpr = "#var.gameName"; ExpressionParser elParser = new SpelExpressionParser(); Object readValue = elParser.parseExpression(readExpr).getValue(safeContxt); System.out.println(readValue); System.out.println("===测试只读模式 修改值==="); String wirteExpr = "#var.gameName='地心侠士 666'"; try { elParser.parseExpression(wirteExpr).getValue(safeContxt); System.out.println("属性修改成功"); } catch (Exception e) { System.out.println("属性内容修改失败:" + e.getMessage()); } System.out.println("===测试安全模式 调用方法==="); String elMethod = "#var.getGameName()"; try { elParser.parseExpression(elMethod).getValue(safeContxt); } catch (Exception e) { System.out.println("方法调用失败:" + e.getMessage()); } System.out.println(testObject.toString()); }

运行结果如下:

===测试只读模式 读取值=== 地心侠士 ===测试只读模式 修改值=== 属性内容修改失败:EL1068E: The expression component '#var.gameName='地心侠士 666'' is not assignable ===测试安全模式 调用方法=== 方法调用失败:EL1004E: Method call: Method getGameName() cannot be found on type com.herbert.script.SpringElScriptSafeAndExtend$ElTestObject ElTestObject [gameType=小游戏, gameName=地心侠士]

从测试结果可以知道SimpleEvaluationContext需要在代码明确指定可访问的属性,比如上边的测试代码没有调用withMethodResolvers就不能调用对象方法.

关键代码:SimpleEvaluationContext.forReadOnlyDataBinding().build()

9. 总结

需要限制引擎能力,主要需要主要从类,属性,方法,内容层面限制.

  • 限制类 :org.springframework.expression.TypeLocator
  • 限制属性:org.springframework.expression.PropertyAccessor
  • 限制方法:org.springframework.expression.MethodResolver
  • 限制内容:org.springframework.expression.TypeConverter
  • 限制bean:org.springframework.expression.BeanResolver

扩展主要体现在扩展方法操作符重写扩展索引访问

  • 扩展方法:ctx.registerFunction(String, Method)
  • 操作符重写:org.springframework.expression.OperatorOverloader
  • 扩展索引访问:org.springframework.expression.IndexAccessor
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/25 23:08:27

MELD多模态情感识别:如何让AI真正理解对话中的情感变化?

MELD多模态情感识别&#xff1a;如何让AI真正理解对话中的情感变化&#xff1f; 【免费下载链接】MELD MELD: A Multimodal Multi-Party Dataset for Emotion Recognition in Conversation 项目地址: https://gitcode.com/gh_mirrors/mel/MELD 在人工智能快速发展的今天…

作者头像 李华
网站建设 2026/1/22 13:44:01

创意AI应用开发大赛 - 基于Google AI Studio的创新实践指南

&#x1f3c6; 创意AI应用开发大赛 - 基于Google AI Studio的创新实践指南 大赛主题&#xff1a;基于Google AI Studio构建创新性人工智能解决方案 适合人群&#xff1a;AI开发者、创新者、学生、技术爱好者 技术栈&#xff1a;Google AI Studio, Gemini API, Python, JavaScri…

作者头像 李华
网站建设 2026/1/29 2:25:59

AI是风口还是泡沫?一个独立开发者的冷思考

【一】 最近大家都在谈AI&#xff0c;有人说靠AI做副业月入几万&#xff0c;也有人说AI是泡沫&#xff0c;投进去都打水漂。 作为一个独立开发者&#xff0c;也一直在跟AI打交道。今天想聊一聊这个话题&#xff1a;AI到底是风口&#xff0c;还是泡沫&#xff1f; 先说自己的…

作者头像 李华
网站建设 2026/1/29 16:43:33

喜马拉雅下载工具终极指南:快速实现离线音频批量管理

还在为无法随时随地收听喜马拉雅音频而烦恼吗&#xff1f;这款喜马拉雅下载工具将彻底改变你的收听体验&#xff0c;让你轻松获取会员和付费内容&#xff0c;实现真正的音频自由&#xff01;无论你是忙碌的上班族&#xff0c;还是热爱学习的知识追求者&#xff0c;都能通过这个…

作者头像 李华
网站建设 2026/1/30 2:25:27

校务管理|基于Java+ vue校务管理系统(源码+数据库+文档)

校务管理 目录 基于springboot vue校务管理系统 一、前言 二、系统功能演示 ​编辑 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue校务管理系统 一、前言 博主介绍&#xff…

作者头像 李华
网站建设 2026/1/22 14:10:08

酒店预约|基于Java+ vue酒店预约系统(源码+数据库+文档)

酒店预约 目录 基于springboot vue酒店预约系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue酒店预约系统 一、前言 博主介绍&#xff1a;✌️大…

作者头像 李华