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