当前位置: 首页 > news >正文

spring中el表达式安全和扩展

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));}@Overridepublic 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 {@Overridepublic 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);}@Overridepublic 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("小游戏", "地心侠士");@Overridepublic 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 {@Overridepublic @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);// 内部调用 setVariablectx.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 {@Overridepublic 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;}@Overridepublic 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

以上有完整测试代码,如有需要,请在微信公众号:小满小慢 回复spelsafe获取完整测试代码.

原文地址:https://mp.weixin.qq.com/s/jzkiCvMLVJVBjCCLHIn4Ww

http://www.jsqmd.com/news/76687/

相关文章:

  • 5分钟搞定多人会议记录:Sortformer说话人区分实战指南
  • 2025年口碑好的广告机、立式广告机及合作案例多的广告机生产 - myqiye
  • 2025年十大口碑好的办公设计公司推荐,看看哪家价格合理? - 工业品牌热点
  • 2025年有实力的宿舍铁床款式/学校宿舍铁床优质厂家推荐榜单 - 品牌宣传支持者
  • 测试环境配置与虚拟化技术:构建高效、可靠的质量保障基石
  • SQLServer 2019 标准版在虚拟机上无法充分利用CPU的问题诊断
  • 北京格微建设工程有限公司的行业认可度高吗?研发能力强吗 - 工业推荐榜
  • 2025年专业的烤漆龙骨厂家推荐及采购指南 - 品牌宣传支持者
  • 天爱验证码:Java项目安全验证的终极解决方案
  • 第11.1节 混合储能系统基本原理
  • HTML一键打包EXE最新2026含免费内核使用说明介绍-附下载地址
  • 2025年比较好的温室大棚行业内口碑厂家排行榜 - 品牌宣传支持者
  • Citra模拟器终极指南:5步快速畅玩3DS游戏
  • 2025年浙江可靠的GEO企业哪个好,广告全案策划、制作、发布/GEO服务/节目内容策划制作/GEO优化AI工具排名GEO老牌厂家排行榜单 - 品牌推荐师
  • 用1/10的成本跑RAG?向量压缩+模型蒸馏+智能缓存实战指南
  • 从 “道、法、术、器、势“ 看量化交易:A 股实战指南
  • 毕业设计实战:基于SpringBoot+MySQL的机动车号牌管理系统,从0到1避坑全流程,导师都说稳!
  • LM完成教程:基于 nanochat项目 从零开始理解大语言模型
  • 达梦数据库V8视图和索引实战指南
  • Linux终端基础操作指南:从入门到避坑
  • FastAPI框架
  • 高密度互联:连接AI“积木”的精密桥梁
  • 你还在 for 循环里使用 await?异步循环得这样写
  • 2025年评价高的学生公寓床/智能公寓床厂家最新用户好评榜 - 品牌宣传支持者
  • 如何用新榜小豆芽解决自媒体团队最头疼的3大难题?
  • 新榜小豆芽全场景运维指南:多账号管理能力与常见故障精准排查
  • Leetcode 76 必须拿起的最小连续卡牌数 | 可互换矩形的组数
  • 2025年有实力托辊式网带炉/无马弗网带炉行业内知名厂家排行榜 - 品牌宣传支持者
  • 前端vue方案在vscode使用插件部署到服服务器的手段
  • 程序员应该熟悉的概念(6)Fine-tuning和RAG