mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2026-05-29 18:57:11 +08:00
修复ExpressionEngine中SpELEngine、MVEL白名单无效问题
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user