This is an automated email from the ASF dual-hosted git repository.
funky-eyes pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new 82980d57d1 optimize: optimize Saga-SpringEL (#8104)
82980d57d1 is described below
commit 82980d57d1b11ade776fab29f546c8233d9c740b
Author: jimin <[email protected]>
AuthorDate: Sat May 30 21:40:35 2026 +0800
optimize: optimize Saga-SpringEL (#8104)
---
.../pcext/handlers/ScriptTaskStateHandler.java | 27 +++++
.../serializer/impl/ExceptionSerializer.java | 42 ++++++-
.../src/test/java/com/test/MaliciousPayload.java} | 16 +--
.../pcext/handlers/ScriptTaskStateHandlerTest.java | 107 +++++++++++++++++
.../serializer/impl/ExceptionSerializerTest.java | 128 +++++++++++++++++++++
.../engine/expression/spel/SpringELExpression.java | 18 +++
.../expression/spel/SpringELExpressionFactory.java | 24 +++-
.../spel/SpringELExpressionFactoryTest.java | 39 ++++++-
.../hessian/FieldCountCappingDeserializer.java | 92 +++++++++++++++
9 files changed, 473 insertions(+), 20 deletions(-)
diff --git
a/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java
b/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java
index 40b4d72236..0707332a7a 100644
---
a/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java
+++
b/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java
@@ -37,9 +37,13 @@ import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleBindings;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
/**
* ScriptTaskState Handler
@@ -49,6 +53,16 @@ public class ScriptTaskStateHandler implements StateHandler,
InterceptableStateH
private static final Logger LOGGER =
LoggerFactory.getLogger(ScriptTaskStateHandler.class);
+ private static final Set<String> ALLOWED_SCRIPT_TYPES = new
HashSet<>(Arrays.asList("groovy", "js", "javascript"));
+
+ private static final Pattern DANGEROUS_PATTERN = Pattern.compile("(?i)"
+ + "ProcessBuilder|\\.execute\\s*\\("
+ + "|System\\s*\\.\\s*(exit|getRuntime|setSecurityManager)"
+ + "|Class\\s*\\.\\s*forName|ClassLoader"
+ + "|java\\.lang\\.reflect"
+ + "|java\\.io\\.File|java\\.net\\."
+ + "|GroovyShell|GroovyClassLoader");
+
private List<StateHandlerInterceptor> interceptors = new ArrayList<>();
private volatile Map<String, ScriptEngine> scriptEngineCache = new
ConcurrentHashMap<>();
@@ -104,6 +118,8 @@ public class ScriptTaskStateHandler implements
StateHandler, InterceptableStateH
}
}
}
+ validateScriptSecurity(scriptType, scriptContent);
+
if (bindings != null) {
result = scriptEngine.eval(scriptContent, bindings);
} else {
@@ -137,6 +153,17 @@ public class ScriptTaskStateHandler implements
StateHandler, InterceptableStateH
}
}
+ static void validateScriptSecurity(String scriptType, String
scriptContent) {
+ if (scriptType != null &&
!ALLOWED_SCRIPT_TYPES.contains(scriptType.toLowerCase())) {
+ throw new EngineExecutionException(
+ "Disallowed script type: " + scriptType,
FrameworkErrorCode.ParameterRequired);
+ }
+ if (scriptContent != null &&
DANGEROUS_PATTERN.matcher(scriptContent).find()) {
+ throw new EngineExecutionException(
+ "Script content contains disallowed dangerous code
pattern", FrameworkErrorCode.ParameterRequired);
+ }
+ }
+
protected ScriptEngine getScriptEngineFromCache(String scriptType,
ScriptEngineManager scriptEngineManager) {
return CollectionUtils.computeIfAbsent(
scriptEngineCache, scriptType, key ->
scriptEngineManager.getEngineByName(scriptType));
diff --git
a/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java
b/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java
index 997f4ace6d..7891577d84 100644
---
a/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java
+++
b/saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java
@@ -16,6 +16,8 @@
*/
package org.apache.seata.saga.engine.serializer.impl;
+import org.apache.seata.common.exception.ErrorCode;
+import org.apache.seata.common.exception.SeataRuntimeException;
import org.apache.seata.saga.engine.serializer.Serializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,6 +27,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.util.Arrays;
+import java.util.List;
/**
* Exception serializer
@@ -34,6 +39,31 @@ public class ExceptionSerializer implements
Serializer<Exception, byte[]> {
private static final Logger LOGGER =
LoggerFactory.getLogger(ExceptionSerializer.class);
+ private static final List<String> ALLOWED_CLASS_PREFIXES =
+ Arrays.asList("java.", "javax.", "org.apache.seata.", "[B",
"io.seata.", "org.springframework.");
+
+ static boolean isClassAllowed(String className) {
+ if (className.startsWith("[")) {
+ String stripped = className;
+ while (stripped.startsWith("[")) {
+ stripped = stripped.substring(1);
+ }
+ if (stripped.length() == 1) {
+ return true;
+ }
+ if (stripped.startsWith("L") && stripped.endsWith(";")) {
+ return isClassAllowed(stripped.substring(1, stripped.length()
- 1));
+ }
+ return false;
+ }
+ for (String prefix : ALLOWED_CLASS_PREFIXES) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public static byte[] serializeByObjectOutput(Object o) {
byte[] result = null;
@@ -66,7 +96,17 @@ public class ExceptionSerializer implements
Serializer<Exception, byte[]> {
Object result = null;
if (bytes != null) {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
- try (ObjectInputStream ois = new ObjectInputStream(bais)) {
+ try (ObjectInputStream ois = new ObjectInputStream(bais) {
+ @Override
+ protected Class<?> resolveClass(ObjectStreamClass desc) throws
IOException, ClassNotFoundException {
+ if (!isClassAllowed(desc.getName())) {
+ throw new SeataRuntimeException(
+ ErrorCode.ERR_DESERIALIZATION_SECURITY,
+ "Failed to deserialize object: " +
desc.getName() + " is not permitted");
+ }
+ return super.resolveClass(desc);
+ }
+ }) {
result = ois.readObject();
} catch (IOException e) {
LOGGER.error("deserialize failed:", e);
diff --git
a/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
b/saga/seata-saga-engine/src/test/java/com/test/MaliciousPayload.java
similarity index 64%
copy from
saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
copy to saga/seata-saga-engine/src/test/java/com/test/MaliciousPayload.java
index d49529da49..8f4a5b7cd1 100644
---
a/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
+++ b/saga/seata-saga-engine/src/test/java/com/test/MaliciousPayload.java
@@ -14,18 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.seata.saga.engine.expression.spel;
+package com.test;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
+import java.io.Serializable;
-/**
- * SpringELExpressionFactoryTest
- */
-public class SpringELExpressionFactoryTest {
- @Test
- public void testCreateExpression() {
- SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
- Assertions.assertNotNull(factory.createExpression("'Hello
World'.concat('!')"));
- }
+public class MaliciousPayload implements Serializable {
+ private static final long serialVersionUID = 1L;
}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandlerTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandlerTest.java
new file mode 100644
index 0000000000..e2fbb3e097
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandlerTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.handlers;
+
+import org.apache.seata.saga.engine.exception.EngineExecutionException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ScriptTaskStateHandlerTest {
+
+ @Test
+ void testValidateScriptSecurity_allowsNormalGroovyScript() {
+ assertDoesNotThrow(() ->
ScriptTaskStateHandler.validateScriptSecurity("groovy", "a + b"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_allowsJavascript() {
+ assertDoesNotThrow(() ->
ScriptTaskStateHandler.validateScriptSecurity("js", "var x = 1 + 2; x;"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksDisallowedScriptType() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("python",
"print('hello')"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_allowsRuntimeException() {
+ assertDoesNotThrow(
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"throw new RuntimeException('test')"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksGroovyExecute() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"'whoami'.execute()"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksProcessBuilder() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity(
+ "groovy", "new ProcessBuilder(['whoami']).start()"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksClassForName() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"Class.forName('java.lang.Runtime')"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksSystemExit() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"System.exit(0)"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksFileAccess() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"new java.io.File('/etc/passwd').text"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksNetworkAccess() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity(
+ "groovy", "new java.net.URL('http://evil.com').text"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksGroovyShell() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"new GroovyShell().evaluate('1+1')"));
+ }
+
+ @Test
+ void testValidateScriptSecurity_blocksClassLoader() {
+ assertThrows(
+ EngineExecutionException.class,
+ () -> ScriptTaskStateHandler.validateScriptSecurity("groovy",
"new ClassLoader(){}"));
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializerTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializerTest.java
new file mode 100644
index 0000000000..0955540202
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializerTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.serializer.impl;
+
+import com.test.MaliciousPayload;
+import org.apache.seata.common.exception.SeataRuntimeException;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ExceptionSerializerTest {
+
+ @Test
+ void testSerializeAndDeserializeException() {
+ ExceptionSerializer serializer = new ExceptionSerializer();
+ RuntimeException original = new RuntimeException("test error");
+
+ byte[] bytes = serializer.serialize(original);
+ assertNotNull(bytes);
+
+ Exception deserialized = serializer.deserialize(bytes);
+ assertNotNull(deserialized);
+ assertEquals("test error", deserialized.getMessage());
+ assertTrue(deserialized instanceof RuntimeException);
+ }
+
+ @Test
+ void testDeserializeNull() {
+ ExceptionSerializer serializer = new ExceptionSerializer();
+ assertNull(serializer.deserialize(null));
+ }
+
+ @Test
+ void testSerializeNull() {
+ assertNull(ExceptionSerializer.serializeByObjectOutput(null));
+ }
+
+ @Test
+ void testIsClassAllowed_javaLang() {
+
assertTrue(ExceptionSerializer.isClassAllowed("java.lang.RuntimeException"));
+ }
+
+ @Test
+ void testIsClassAllowed_seataPackage() {
+ assertTrue(
+
ExceptionSerializer.isClassAllowed("org.apache.seata.saga.engine.exception.EngineExecutionException"));
+ }
+
+ @Test
+ void testIsClassAllowed_byteArray() {
+ assertTrue(ExceptionSerializer.isClassAllowed("[B"));
+ }
+
+ @Test
+ void testIsClassAllowed_allowsJavaArray() {
+
assertTrue(ExceptionSerializer.isClassAllowed("[Ljava.lang.StackTraceElement;"));
+ }
+
+ @Test
+ void testIsClassAllowed_allowsPrimitiveArray() {
+ assertTrue(ExceptionSerializer.isClassAllowed("[B"));
+ assertTrue(ExceptionSerializer.isClassAllowed("[I"));
+ }
+
+ @Test
+ void testIsClassAllowed_blocksUnknownClass() {
+
assertFalse(ExceptionSerializer.isClassAllowed("com.evil.MaliciousPayload"));
+ }
+
+ @Test
+ void testIsClassAllowed_blocksUnknownArray() {
+
assertFalse(ExceptionSerializer.isClassAllowed("[Lcom.evil.MaliciousPayload;"));
+ }
+
+ @Test
+ void testIsClassAllowed_blocksCommonGadgetChain() {
+
assertFalse(ExceptionSerializer.isClassAllowed("org.apache.commons.collections.functors.InvokerTransformer"));
+ }
+
+ @Test
+ void testDeserializeBlocksMaliciousClass() throws Exception {
+ MaliciousPayload payload = new MaliciousPayload();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(payload);
+ }
+ byte[] maliciousBytes = baos.toByteArray();
+
+ assertThrows(
+ SeataRuntimeException.class, () ->
ExceptionSerializer.deserializeByObjectInputStream(maliciousBytes));
+ }
+
+ @Test
+ void testDeserializeWithTypeBlocksMaliciousClass() throws Exception {
+ MaliciousPayload payload = new MaliciousPayload();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(payload);
+ }
+ byte[] maliciousBytes = baos.toByteArray();
+
+ assertThrows(
+ SeataRuntimeException.class,
+ () ->
ExceptionSerializer.deserializeByObjectInputStream(maliciousBytes,
Exception.class));
+ }
+}
diff --git
a/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpression.java
b/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpression.java
index ac1e14e70e..0b99b9160b 100644
---
a/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpression.java
+++
b/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpression.java
@@ -17,6 +17,8 @@
package org.apache.seata.saga.engine.expression.spel;
import org.apache.seata.saga.engine.expression.ELExpression;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* Expression base on Spring EL
@@ -25,18 +27,34 @@ import org.apache.seata.saga.engine.expression.ELExpression;
public class SpringELExpression implements ELExpression {
private org.springframework.expression.Expression expression;
+ private EvaluationContext evaluationContext;
public SpringELExpression(org.springframework.expression.Expression
expression) {
this.expression = expression;
}
+ public SpringELExpression(
+ org.springframework.expression.Expression expression,
EvaluationContext evaluationContext) {
+ this.expression = expression;
+ this.evaluationContext = evaluationContext;
+ }
+
@Override
public Object getValue(Object elContext) {
+ if (evaluationContext instanceof StandardEvaluationContext) {
+ ((StandardEvaluationContext)
evaluationContext).setRootObject(elContext);
+ return expression.getValue(evaluationContext);
+ }
return expression.getValue(elContext);
}
@Override
public void setValue(Object value, Object elContext) {
+ if (evaluationContext instanceof StandardEvaluationContext) {
+ ((StandardEvaluationContext)
evaluationContext).setRootObject(elContext);
+ expression.setValue(evaluationContext, value);
+ return;
+ }
expression.setValue(elContext, value);
}
diff --git
a/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactory.java
b/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactory.java
index 44e3585771..f72f751155 100644
---
a/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactory.java
+++
b/saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactory.java
@@ -22,8 +22,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.EvaluationException;
import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpression;
+import org.springframework.expression.TypeLocator;
+import org.springframework.expression.spel.SpelEvaluationException;
+import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -43,9 +46,22 @@ public class SpringELExpressionFactory implements
ExpressionFactory {
@Override
public Expression createExpression(String expression) {
org.springframework.expression.Expression defaultExpression =
parser.parseExpression(expression);
- EvaluationContext evaluationContext = ((SpelExpression)
defaultExpression).getEvaluationContext();
- ((StandardEvaluationContext) evaluationContext).setBeanResolver(new
AppContextBeanResolver());
- return new SpringELExpression(defaultExpression);
+ StandardEvaluationContext evaluationContext =
createRestrictedEvaluationContext();
+ return new SpringELExpression(defaultExpression, evaluationContext);
+ }
+
+ private StandardEvaluationContext createRestrictedEvaluationContext() {
+ StandardEvaluationContext context = new StandardEvaluationContext();
+ context.setTypeLocator(new DenyAllTypeLocator());
+ context.setBeanResolver(new AppContextBeanResolver());
+ return context;
+ }
+
+ private static class DenyAllTypeLocator implements TypeLocator {
+ @Override
+ public Class<?> findType(String typeName) throws EvaluationException {
+ throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND,
typeName);
+ }
}
private class AppContextBeanResolver implements BeanResolver {
diff --git
a/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
b/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
index d49529da49..891f4f2a48 100644
---
a/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
+++
b/saga/seata-saga-spring/src/test/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactoryTest.java
@@ -16,16 +16,49 @@
*/
package org.apache.seata.saga.engine.expression.spel;
+import org.apache.seata.saga.engine.expression.Expression;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.springframework.expression.spel.SpelEvaluationException;
+
+import java.util.HashMap;
+import java.util.Map;
-/**
- * SpringELExpressionFactoryTest
- */
public class SpringELExpressionFactoryTest {
@Test
public void testCreateExpression() {
SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
Assertions.assertNotNull(factory.createExpression("'Hello
World'.concat('!')"));
}
+
+ @Test
+ public void testPropertyAccessWorks() {
+ SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
+ Expression expression = factory.createExpression("[\"name\"]");
+ Map<String, Object> context = new HashMap<>();
+ context.put("name", "seata");
+ Object value = expression.getValue(context);
+ Assertions.assertEquals("seata", value);
+ }
+
+ @Test
+ public void testTypeAccessBlocked_Runtime() {
+ SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
+ Expression expression =
factory.createExpression("T(Runtime).getRuntime().exec('whoami')");
+ Assertions.assertThrows(SpelEvaluationException.class, () ->
expression.getValue(new HashMap<>()));
+ }
+
+ @Test
+ public void testTypeAccessBlocked_System() {
+ SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
+ Expression expression =
factory.createExpression("T(System).getProperty('user.name')");
+ Assertions.assertThrows(SpelEvaluationException.class, () ->
expression.getValue(new HashMap<>()));
+ }
+
+ @Test
+ public void testTypeAccessBlocked_ProcessBuilder() {
+ SpringELExpressionFactory factory = new
SpringELExpressionFactory(null);
+ Expression expression =
factory.createExpression("T(java.lang.ProcessBuilder).new({'whoami'}).start()");
+ Assertions.assertThrows(SpelEvaluationException.class, () ->
expression.getValue(new HashMap<>()));
+ }
}
diff --git
a/serializer/seata-serializer-hessian/src/main/java/org/apache/seata/serializer/hessian/FieldCountCappingDeserializer.java
b/serializer/seata-serializer-hessian/src/main/java/org/apache/seata/serializer/hessian/FieldCountCappingDeserializer.java
new file mode 100644
index 0000000000..0a590b6416
--- /dev/null
+++
b/serializer/seata-serializer-hessian/src/main/java/org/apache/seata/serializer/hessian/FieldCountCappingDeserializer.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.serializer.hessian;
+
+import com.caucho.hessian.io.AbstractHessianInput;
+import com.caucho.hessian.io.Deserializer;
+
+import java.io.IOException;
+
+/**
+ * Delegates to a Hessian {@link Deserializer} but rejects absurd
+ * {@code createFields(len)} values before the delegate allocates proportional
+ * to {@code len} (mitigates tiny-payload OOM on malicious Hessian class defs).
+ */
+final class FieldCountCappingDeserializer implements Deserializer {
+
+ private final Deserializer delegate;
+ private final int maxClassFields;
+
+ FieldCountCappingDeserializer(Deserializer delegate, int maxClassFields) {
+ this.delegate = delegate;
+ this.maxClassFields = maxClassFields;
+ }
+
+ @Override
+ public Class<?> getType() {
+ return delegate.getType();
+ }
+
+ @Override
+ public boolean isReadResolve() {
+ return delegate.isReadResolve();
+ }
+
+ @Override
+ public Object readObject(AbstractHessianInput in) throws IOException {
+ return delegate.readObject(in);
+ }
+
+ @Override
+ public Object readList(AbstractHessianInput in, int length) throws
IOException {
+ return delegate.readList(in, length);
+ }
+
+ @Override
+ public Object readLengthList(AbstractHessianInput in, int length) throws
IOException {
+ return delegate.readLengthList(in, length);
+ }
+
+ @Override
+ public Object readMap(AbstractHessianInput in) throws IOException {
+ return delegate.readMap(in);
+ }
+
+ @Override
+ public Object[] createFields(int len) {
+ if (len < 0 || len > maxClassFields) {
+ throw new IllegalStateException(
+ "Hessian class definition field count " + len + " exceeds
maximum " + maxClassFields);
+ }
+ return delegate.createFields(len);
+ }
+
+ @Override
+ public Object createField(String name) {
+ return delegate.createField(name);
+ }
+
+ @Override
+ public Object readObject(AbstractHessianInput in, Object[] fields) throws
IOException {
+ return delegate.readObject(in, fields);
+ }
+
+ @Override
+ public Object readObject(AbstractHessianInput in, String[] fieldNames)
throws IOException {
+ return delegate.readObject(in, fieldNames);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]