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]

Reply via email to