Copilot commented on code in PR #8104:
URL: https://github.com/apache/incubator-seata/pull/8104#discussion_r3296792041


##########
saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java:
##########
@@ -104,6 +120,8 @@ public void process(ProcessContext context) throws 
EngineExecutionException {
                     }
                 }
             }
+            validateScriptSecurity(scriptType, scriptContent);
+

Review Comment:
   `validateScriptSecurity` runs after `getScriptEngineFromCache(...)` has 
already been called. For disallowed/invalid `scriptType`, this can still 
create/cache a `ScriptEngine` (and for `null` scriptType it can NPE inside 
`computeIfAbsent`). Validate `scriptType`/`scriptContent` before interacting 
with the cache/engine manager so failures are deterministic and side-effect 
free.



##########
saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java:
##########
@@ -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;
+    }

Review Comment:
   The hard-coded `ALLOWED_CLASS_PREFIXES` only permits a small set of package 
prefixes. Saga persists whatever `Exception` is thrown during execution (often 
business exceptions from application packages), so this change can cause 
state-log recovery/query to start failing with `SeataRuntimeException` when 
encountering previously-serialized user exceptions. Consider making the 
allowlist configurable/extendable (or deserializing into a safe placeholder 
exception when a class is blocked) to avoid breaking persisted data while still 
enforcing security.



##########
saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpression.java:
##########
@@ -25,18 +27,34 @@
 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;

Review Comment:
   `SpringELExpression` mutates the shared `StandardEvaluationContext` by 
calling `setRootObject` before every evaluation. These `Expression` instances 
are cached and reused (e.g., `ChoiceStateHandler` stores evaluators in the 
state definition), so concurrent executions can race and read/write the wrong 
root object. Prefer using the SpEL overloads that accept a root object (e.g., 
`expression.getValue(evaluationContext, elContext)` / 
`setValue(evaluationContext, elContext, value)`) or create/clone a context per 
evaluation to keep this thread-safe.



##########
saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/serializer/impl/ExceptionSerializer.java:
##########
@@ -66,7 +96,17 @@ public static Object deserializeByObjectInputStream(byte[] 
bytes) {
         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);
+                }
+            }) {

Review Comment:
   Only `resolveClass` is overridden for the allowlist check. Java 
deserialization of dynamic proxies uses `resolveProxyClass(String[] 
interfaces)` (which loads interface classes directly) and can bypass 
`resolveClass`-only filtering. Add an override of `resolveProxyClass` that 
validates each interface name with `isClassAllowed` (or switch to 
`ObjectInputFilter`) to close this gap.



##########
saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java:
##########
@@ -137,6 +155,17 @@ public void process(ProcessContext context) throws 
EngineExecutionException {
         }
     }
 
+    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);
+        }

Review Comment:
   `validateScriptSecurity` throws `FrameworkErrorCode.ParameterRequired`, but 
these failures are “invalid parameter” cases (see 
`FrameworkErrorCode.InvalidParameter`). Also, `scriptType.toLowerCase()` should 
use `Locale.ROOT` to avoid locale-sensitive casing bugs. Consider treating 
null/blank `scriptType` as invalid here to avoid later NPEs in the 
script-engine cache.



##########
saga/seata-saga-spring/src/main/java/org/apache/seata/saga/engine/expression/spel/SpringELExpressionFactory.java:
##########
@@ -43,9 +46,22 @@ public SpringELExpressionFactory(ApplicationContext 
applicationContext) {
     @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;

Review Comment:
   PR description/title only mentions “optimize Saga-SpringEL”, but this PR 
also changes Java deserialization security (`ExceptionSerializer`), script 
execution security (`ScriptTaskStateHandler`), and adds a Hessian deserializer 
helper. Please update the PR description/title to reflect the full scope, or 
split into focused PRs to simplify review and rollout.



##########
saga/seata-saga-engine/src/main/java/org/apache/seata/saga/engine/pcext/handlers/ScriptTaskStateHandler.java:
##########
@@ -49,6 +53,18 @@ 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)"
+            + "Runtime|ProcessBuilder|\\.execute\\s*\\("
+            + "|System\\s*\\.\\s*(exit|getRuntime|setSecurityManager)"
+            + "|Class\\s*\\.\\s*forName|ClassLoader"
+            + "|java\\.lang\\.reflect"
+            + "|Thread\\s*\\.|\\.getClass\\s*\\("
+            + "|java\\.io\\.File|java\\.net\\."
+            + "|javax\\.script\\.ScriptEngine"
+            + "|GroovyShell|GroovyClassLoader");

Review Comment:
   The `DANGEROUS_PATTERN` blacklist is easy to bypass because several tokens 
require exact dot formatting (e.g., `java\.io\.File` won’t match `java 
.io.File` or commented/concatenated variants). If this is meant as a security 
boundary, consider using a stricter whitelist/sandbox for the scripting engine 
or normalizing/parsing the script content rather than relying on a simple regex 
blacklist.



##########
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);
+    }
+}

Review Comment:
   `FieldCountCappingDeserializer` is added but not referenced anywhere in the 
Hessian serializer path (no usages in 
`HessianSerializer`/`HessianSerializerFactory`). As-is it provides no 
protection and adds dead code. Either wire it into the Hessian 
`SerializerFactory`/deserializer creation flow (wrapping delegates) or remove 
it until it’s used.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to