修复ExpressionEngine中SpELEngine、MVEL白名单无效问题

This commit is contained in:
Looly
2026-05-09 10:31:25 +08:00
parent 49858088d3
commit 9505fdab70
6 changed files with 131 additions and 15 deletions

View File

@@ -1,13 +1,16 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.45(2026-04-07)
# 5.8.45(2026-05-09)
### 🐣新特性
* 【core 】 `AnnotationUtil`新增两级缓存架构提升高频注解解析性能pr#1434@Gitee
### 🐞Bug修复
* 【db 】 修复`Page``PageResult`首页调用问题issue#IH7A18@Gitee
* 【ai 】 修复AI SPI classloader找不到实现问题issue#4241@Github
* 【extra 】 修复`ExpressionEngine`中SpELEngine、MVEL白名单无效问题issue#4249@Github
* 【core 】 修复`JNDIUtil`远程加载漏洞issue#4249@Github
* 【core 】 修复`ValidateObjectInputStream`白名单规则问题issue#4249@Github
-------------------------------------------------------------------------------------------------------------
# 5.8.44(2026-03-11)

View File

@@ -85,14 +85,7 @@ public class ValidateObjectInputStream extends ObjectInputStream {
}
}
if(CollUtil.isEmpty(this.whiteClassSet)){
return;
}
if(className.startsWith("java.")){
// java中的类默认在白名单中
return;
}
if(this.whiteClassSet.contains(className)){
if(CollUtil.isEmpty(this.whiteClassSet) || this.whiteClassSet.contains(className)){
return;
}

View File

@@ -8,7 +8,9 @@ import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
/**
@@ -20,15 +22,20 @@ import java.util.Map;
* 见https://blog.csdn.net/u010430304/article/details/54601302
* </p>
*
* @author loolY
* @author looly
* @since 5.7.7
*/
public class JNDIUtil {
/**
* 创建{@link InitialDirContext}
* 建议在应用启动时设置系统属性(禁用远程 codebase 加载)
* <pre>{@code
* System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
* System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
* }</pre>
*
* @param environment 环境参数,{@code null}表示无参数
* @param environment 环境参数,如@{code java.naming.factory.initial}和{@code java.naming.provider.url}{@code null}表示无参数
* @return {@link InitialDirContext}
*/
public static InitialDirContext createInitialDirContext(Map<String, String> environment) {
@@ -36,6 +43,10 @@ public class JNDIUtil {
if (MapUtil.isEmpty(environment)) {
return new InitialDirContext();
}
// issue#4249 修复JNDI注入漏洞
validateEnvironment(environment);
return new InitialDirContext(Convert.convert(Hashtable.class, environment));
} catch (NamingException e) {
throw new UtilException(e);
@@ -43,9 +54,14 @@ public class JNDIUtil {
}
/**
* 创建{@link InitialContext}
* 创建{@link InitialContext}<br>
* 建议在应用启动时设置系统属性(禁用远程 codebase 加载)
* <pre>{@code
* System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
* System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
* }</pre>
*
* @param environment 环境参数,{@code null}表示无参数
* @param environment 环境参数,如@{code java.naming.factory.initial}和{@code java.naming.provider.url}{@code null}表示无参数
* @return {@link InitialContext}
*/
public static InitialContext createInitialContext(Map<String, String> environment) {
@@ -53,6 +69,10 @@ public class JNDIUtil {
if (MapUtil.isEmpty(environment)) {
return new InitialContext();
}
// issue#4249 修复JNDI注入漏洞
validateEnvironment(environment);
return new InitialContext(Convert.convert(Hashtable.class, environment));
} catch (NamingException e) {
throw new UtilException(e);
@@ -74,4 +94,63 @@ public class JNDIUtil {
throw new UtilException(e);
}
}
private static final List<String> SAFE_PROTOCOLS = Arrays.asList(
"java:",
"dns:"
);
/**
* 验证并过滤environment中的危险属性
*
* @param environment 原始环境参数
* @return 过滤后的环境参数
*/
private static Map<String, String> validateEnvironment(Map<String, String> environment) {
if (MapUtil.isNotEmpty(environment)) {
// 检查 PROVIDER_URL
String providerUrl = environment.get("java.naming.provider.url");
if (StrUtil.isNotBlank(providerUrl) && !isSafeProtocol(providerUrl)) {
throw new UtilException("JNDI protocol not allowed: " + providerUrl);
}
// 检查 INITIAL_CONTEXT_FACTORY
String factory = environment.get("java.naming.factory.initial");
if (StrUtil.isNotBlank(factory)) {
// 只允许安全的工厂类
if (!factory.startsWith("com.sun.jndi.dns.") &&
!factory.startsWith("com.sun.jndi.ldap.") &&
!factory.startsWith("com.sun.jndi.rmi.")) {
throw new UtilException("JNDI factory not allowed: " + factory);
}
}
}
return environment;
}
/**
* 检查URL是否在协议白名单内
*
* @param url 要检查的URL
* @param allowedProtocols 允许的协议列表,{@code null}或空表示使用默认安全协议
* @return 是否安全
*/
private static boolean isSafeProtocol(String url, String... allowedProtocols) {
if (StrUtil.isBlank(url)) {
return false;
}
List<String> protocols = (allowedProtocols != null && allowedProtocols.length > 0)
? Arrays.asList(allowedProtocols)
: SAFE_PROTOCOLS;
String lowerUrl = url.toLowerCase();
for (String protocol : protocols) {
if (lowerUrl.startsWith(protocol.toLowerCase())) {
return true;
}
}
return false;
}
}

View File

@@ -1,6 +1,8 @@
package cn.hutool.extra.expression.engine.mvel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.expression.ExpressionEngine;
import cn.hutool.extra.expression.ExpressionException;
import org.mvel2.MVEL;
import java.util.Collection;
@@ -27,6 +29,16 @@ public class MvelEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {
// issue#4249 检查context的value类型是否在白名单中不在则抛出异常
if(CollUtil.isNotEmpty(allowClassSet)){
context.values().forEach(value -> {
if(!allowClassSet.contains(value.getClass())){
throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
}
});
}
return MVEL.eval(expression, context);
}

View File

@@ -1,7 +1,9 @@
package cn.hutool.extra.expression.engine.rhino;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.expression.ExpressionEngine;
import cn.hutool.extra.expression.ExpressionException;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
@@ -24,6 +26,16 @@ public class RhinoEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {
// issue#4249 检查context的value类型是否在白名单中不在则抛出异常
if(CollUtil.isNotEmpty(allowClassSet)){
context.values().forEach(value -> {
if(!allowClassSet.contains(value.getClass())){
throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
}
});
}
final Context ctx = Context.enter();
final Scriptable scope = ctx.initStandardObjects();
if (MapUtil.isNotEmpty(context)) {

View File

@@ -1,10 +1,12 @@
package cn.hutool.extra.expression.engine.spel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.expression.ExpressionEngine;
import cn.hutool.extra.expression.ExpressionException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.Collection;
import java.util.Map;
@@ -29,7 +31,22 @@ public class SpELEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {
final EvaluationContext evaluationContext = new StandardEvaluationContext();
// final EvaluationContext evaluationContext = new StandardEvaluationContext();
// issue#4249 检查context的value类型是否在白名单中不在则抛出异常
if(CollUtil.isNotEmpty(allowClassSet)){
context.values().forEach(value -> {
if(!allowClassSet.contains(value.getClass())){
throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
}
});
}
EvaluationContext evaluationContext = SimpleEvaluationContext
.forReadOnlyDataBinding()
.withInstanceMethods() // 仅允许调用白名单类的实例方法
.build();
context.forEach(evaluationContext::setVariable);
return parser.parseExpression(expression).getValue(evaluationContext);
}