spring中el表达式安全和扩展
spring中el表达式安全和扩展
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
以上有完整测试代码,如有需要,请在微信公众号:小满小慢 回复spelsafe获取完整测试代码.
- 转载请注明来源
- 作者:杨瀚博
- QQ:464884492

浙公网安备 33010602011771号