This is an automated email from the ASF dual-hosted git repository.

jianbin 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 5195e13e34 feature: add whitelist class (#7973)
5195e13e34 is described below

commit 5195e13e346223e4c0cdd94f888c756dc59b9696
Author: legendpei <[email protected]>
AuthorDate: Wed Apr 1 10:53:11 2026 +0800

    feature: add whitelist class (#7973)
---
 changes/en-us/2.x.md                               |   1 +
 changes/zh-cn/2.x.md                               |   1 +
 .../seata/common/json/JsonAllowlistManager.java    | 301 ++++++++++++++++++
 .../common/json/impl/FastjsonJsonSerializer.java   |  34 ++-
 .../common/json/impl/JacksonJsonSerializer.java    |  53 +++-
 .../seata/common/json/FastjsonAllowlistTest.java   | 232 ++++++++++++++
 .../seata/common/json/JacksonAllowlistTest.java    | 249 +++++++++++++++
 .../common/json/JsonAllowlistManagerTest.java      | 340 +++++++++++++++++++++
 script/client/spring/application.properties        |   2 +
 script/client/spring/application.yml               |   2 +
 .../seata-spring-autoconfigure-client/pom.xml      |   9 +
 .../SeataClientEnvironmentPostProcessor.java       |   3 +
 .../properties/SeataJsonProperties.java            |  54 ++++
 .../properties/SeataJsonPropertiesTest.java        |  74 +++++
 .../boot/autoconfigure/StarterConstants.java       |   1 +
 15 files changed, 1352 insertions(+), 4 deletions(-)

diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 7d622bd8a8..0819b534dd 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -24,6 +24,7 @@ Add changes here for all PR submitted to the 2.x branch.
 - [[#7760](https://github.com/apache/incubator-seata/pull/7760)] unify 
Jackson/fastjson serialization
 - [[#7000](https://github.com/apache/incubator-seata/pull/7000)] support 
multi-version codec & fix not returning client registration failure msg.
 - [[#7865](https://github.com/apache/incubator-seata/pull/7865)] add Benchmark 
CLI tool
+- [[#7973](https://github.com/apache/incubator-seata/pull/7973)] Added JSON 
whitelist security mechanism
 - [[#7903](https://github.com/apache/incubator-seata/pull/7903)] Support 
HTTP/2 stream push for the Watch API in Server Raft mode
 - [[#8002](https://github.com/apache/incubator-seata/pull/8002)] add Grafana 
dashboard JSON for NamingServer metrics
 
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index c30bf8c8fb..d89a28f5b7 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -24,6 +24,7 @@
 - [[#7760](https://github.com/apache/incubator-seata/pull/7760)] 
统一Jackson/fastjson序列化器
 - [[#7000](https://github.com/apache/incubator-seata/pull/7000)] 
支持多版本codec,修复不返回客户端注册失败消息的问题
 - [[#7865](https://github.com/apache/incubator-seata/pull/7865)] 新增 Benchmark 
命令行工具
+- [[#7973](https://github.com/apache/incubator-seata/pull/7973)] 添加了 JSON 
白名单安全机制
 - [[#7903](https://github.com/apache/incubator-seata/pull/7903)] 在Server 
Raft模式下支持Watch API的HTTP/2流推送
 - [[#8014](https://github.com/apache/incubator-seata/pull/8014)] 为 benchmark 
CLI 添加 P99.9 尾延迟百分位
 - [[#8002](https://github.com/apache/incubator-seata/pull/8002)] 
为namingserver指标增加Grafana dashboard JSON
diff --git 
a/json-common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java
 
b/json-common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java
new file mode 100644
index 0000000000..f40264d88b
--- /dev/null
+++ 
b/json-common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java
@@ -0,0 +1,301 @@
+/*
+ * 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.common.json;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * JSON deserialization allowlist manager.
+ */
+public class JsonAllowlistManager {
+
+    private static final JsonAllowlistManager INSTANCE = new 
JsonAllowlistManager();
+
+    /**
+     * Built-in exact match allowlist
+     */
+    private final Set<String> builtinClasses = ConcurrentHashMap.newKeySet();
+
+    /**
+     * Built-in prefix match allowlist
+     */
+    private final Set<String> builtinPrefixes = ConcurrentHashMap.newKeySet();
+
+    /**
+     * User-defined exact match allowlist
+     */
+    private final Set<String> userClasses = ConcurrentHashMap.newKeySet();
+
+    /**
+     * User-defined prefix match allowlist
+     */
+    private final Set<String> userPrefixes = ConcurrentHashMap.newKeySet();
+
+    /**
+     * Check result cache (className -> allowed)
+     */
+    private final Map<String, Boolean> cache = new ConcurrentHashMap<>();
+
+    private JsonAllowlistManager() {
+        initBuiltinAllowlist();
+    }
+
+    public static JsonAllowlistManager getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Load user allowlist from configuration string
+     */
+    public void loadUserAllowlist(String config) {
+        userClasses.clear();
+        userPrefixes.clear();
+        cache.clear();
+        if (config == null || config.isEmpty()) {
+            return;
+        }
+        for (String item : config.split(",")) {
+            String trimmed = item.trim();
+            if (trimmed.isEmpty()) {
+                continue;
+            }
+            if (trimmed.endsWith(".")) {
+                userPrefixes.add(trimmed);
+            } else {
+                userClasses.add(trimmed);
+            }
+        }
+    }
+
+    /**
+     * Add a class to user allowlist programmatically
+     */
+    public void addUserClass(String className) {
+        if (className != null && !className.isEmpty()) {
+            userClasses.add(className);
+            cache.remove(className);
+        }
+    }
+
+    /**
+     * Add a prefix to user allowlist programmatically
+     */
+    public void addUserPrefix(String prefix) {
+        if (prefix != null && !prefix.isEmpty()) {
+            userPrefixes.add(prefix);
+            cache.clear();
+        }
+    }
+
+    /**
+     * Check if a class is allowed for deserialization
+     */
+    public boolean isAllowed(String className) {
+        if (className == null) {
+            return false;
+        }
+        return cache.computeIfAbsent(className, this::doCheck);
+    }
+
+    /**
+     * Check if a class is allowed, throw SecurityException if not
+     */
+    public void checkClass(String className) {
+        if (!isAllowed(className)) {
+            throw new SecurityException("Class not in JSON deserialization 
allowlist: " + className
+                    + ". Please add it to seata.json.allowlist 
configuration.");
+        }
+    }
+
+    /**
+     * Clear user allowlist and cache.
+     */
+    public void clearUserAllowlist() {
+        userClasses.clear();
+        userPrefixes.clear();
+        cache.clear();
+    }
+
+    private boolean doCheck(String className) {
+        if (isExactOrPrefixAllowed(className)) {
+            return true;
+        }
+        if (isPrimitiveArrayDescriptor(className)) {
+            return true;
+        }
+        String componentClassName = extractArrayComponentClassName(className);
+        return componentClassName != null && 
isExactOrPrefixAllowed(componentClassName);
+    }
+
+    /**
+     * Check if className is a multi-dimensional primitive array descriptor 
(e.g. "[[I", "[[Z").
+     * Single-dimensional primitive arrays (e.g. "[I") are already in the 
builtin allowlist.
+     */
+    private boolean isPrimitiveArrayDescriptor(String className) {
+        if (className == null || !className.startsWith("[[")) {
+            return false;
+        }
+        String stripped = className;
+        while (stripped.startsWith("[")) {
+            stripped = stripped.substring(1);
+        }
+        return stripped.length() == 1 && 
PRIMITIVE_DESCRIPTORS.indexOf(stripped.charAt(0)) >= 0;
+    }
+
+    private boolean isExactOrPrefixAllowed(String className) {
+        if (builtinClasses.contains(className)) {
+            return true;
+        }
+        for (String prefix : builtinPrefixes) {
+            if (className.startsWith(prefix)) {
+                return true;
+            }
+        }
+        if (userClasses.contains(className)) {
+            return true;
+        }
+        for (String prefix : userPrefixes) {
+            if (className.startsWith(prefix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static final String PRIMITIVE_DESCRIPTORS = "BCSIJFDZ";
+
+    private String extractArrayComponentClassName(String className) {
+        if (className == null || className.isEmpty()) {
+            return null;
+        }
+
+        // Canonical name format: e.g. "int[][]", "String[]"
+        String normalized = className;
+        while (normalized.endsWith("[]")) {
+            normalized = normalized.substring(0, normalized.length() - 2);
+        }
+        if (!normalized.equals(className)) {
+            return normalized;
+        }
+
+        // JVM descriptor format: e.g. "[[I", "[Ljava.lang.String;"
+        if (!normalized.startsWith("[")) {
+            return null;
+        }
+        while (normalized.startsWith("[")) {
+            normalized = normalized.substring(1);
+        }
+        // Primitive descriptor (e.g. "I", "Z") — handled by 
isPrimitiveArrayDescriptor
+        if (normalized.length() == 1) {
+            return null;
+        }
+        // Object descriptor: Lclassname;
+        if (normalized.startsWith("L") && normalized.endsWith(";")) {
+            return normalized.substring(1, normalized.length() - 1);
+        }
+        return null;
+    }
+
+    private void initBuiltinAllowlist() {
+
+        builtinClasses.add("java.lang.Boolean");
+        builtinClasses.add("java.lang.Byte");
+        builtinClasses.add("java.lang.Character");
+        builtinClasses.add("java.lang.Short");
+        builtinClasses.add("java.lang.Integer");
+        builtinClasses.add("java.lang.Long");
+        builtinClasses.add("java.lang.Float");
+        builtinClasses.add("java.lang.Double");
+        builtinClasses.add("java.lang.String");
+        builtinClasses.add("java.lang.Number");
+        builtinClasses.add("java.math.BigDecimal");
+        builtinClasses.add("java.math.BigInteger");
+
+        // byte[]
+        builtinClasses.add("[B");
+        // char[]
+        builtinClasses.add("[C");
+        // short[]
+        builtinClasses.add("[S");
+        // int[]
+        builtinClasses.add("[I");
+        // long[]
+        builtinClasses.add("[J");
+        // float[]
+        builtinClasses.add("[F");
+        // double[]
+        builtinClasses.add("[D");
+        // boolean[]
+        builtinClasses.add("[Z");
+        builtinClasses.add("[Ljava.lang.String;");
+        builtinClasses.add("[Ljava.lang.Object;");
+
+        builtinClasses.add("java.util.Date");
+        builtinClasses.add("java.util.Calendar");
+        builtinClasses.add("java.util.GregorianCalendar");
+        builtinClasses.add("java.sql.Date");
+        builtinClasses.add("java.sql.Time");
+        builtinClasses.add("java.sql.Timestamp");
+        builtinClasses.add("java.time.LocalDateTime");
+        builtinClasses.add("java.time.LocalDate");
+        builtinClasses.add("java.time.LocalTime");
+        builtinClasses.add("java.time.Instant");
+        builtinClasses.add("java.time.Duration");
+        builtinClasses.add("java.time.Period");
+        builtinClasses.add("java.time.ZonedDateTime");
+        builtinClasses.add("java.time.OffsetDateTime");
+        builtinClasses.add("java.time.OffsetTime");
+        builtinClasses.add("java.time.Year");
+        builtinClasses.add("java.time.YearMonth");
+        builtinClasses.add("java.time.MonthDay");
+        builtinClasses.add("java.time.ZoneId");
+        builtinClasses.add("java.time.ZoneOffset");
+
+        builtinClasses.add("java.util.ArrayList");
+        builtinClasses.add("java.util.LinkedList");
+        builtinClasses.add("java.util.Vector");
+        builtinClasses.add("java.util.Stack");
+        builtinClasses.add("java.util.HashSet");
+        builtinClasses.add("java.util.LinkedHashSet");
+        builtinClasses.add("java.util.TreeSet");
+        builtinClasses.add("java.util.HashMap");
+        builtinClasses.add("java.util.LinkedHashMap");
+        builtinClasses.add("java.util.TreeMap");
+        builtinClasses.add("java.util.Hashtable");
+        builtinClasses.add("java.util.Properties");
+        builtinClasses.add("java.util.concurrent.ConcurrentHashMap");
+        builtinClasses.add("java.util.concurrent.CopyOnWriteArrayList");
+        builtinClasses.add("java.util.concurrent.CopyOnWriteArraySet");
+        builtinClasses.add("java.util.concurrent.ConcurrentSkipListMap");
+        builtinClasses.add("java.util.concurrent.ConcurrentSkipListSet");
+
+        builtinClasses.add("java.util.UUID");
+        builtinClasses.add("java.util.Locale");
+        builtinClasses.add("java.util.Currency");
+        builtinClasses.add("java.util.Optional");
+        builtinClasses.add("java.net.URL");
+        builtinClasses.add("java.net.URI");
+        builtinClasses.add("java.io.File");
+        builtinClasses.add("java.nio.file.Path");
+        builtinClasses.add("java.util.regex.Pattern");
+
+        builtinPrefixes.add("org.apache.seata.");
+        builtinPrefixes.add("io.seata.");
+    }
+}
diff --git 
a/json-common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java
 
b/json-common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java
index cac1cf7ac7..42422a639c 100644
--- 
a/json-common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java
+++ 
b/json-common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java
@@ -18,8 +18,10 @@ package org.apache.seata.common.json.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.parser.Feature;
+import com.alibaba.fastjson.parser.ParserConfig;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import org.apache.seata.common.exception.JsonParseException;
+import org.apache.seata.common.json.JsonAllowlistManager;
 import org.apache.seata.common.json.JsonSerializer;
 import org.apache.seata.common.loader.LoadLevel;
 
@@ -56,6 +58,16 @@ public class FastjsonJsonSerializer implements 
JsonSerializer {
     private static final Feature[] READER_FEATURES_IGNORE_AUTO_TYPE =
             new Feature[] {Feature.IgnoreAutoType, Feature.OrderedField};
 
+    private static final ParserConfig ALLOWLIST_PARSER_CONFIG = new 
ParserConfig();
+
+    static {
+        ALLOWLIST_PARSER_CONFIG.setAutoTypeSupport(true);
+        ALLOWLIST_PARSER_CONFIG.addAutoTypeCheckHandler((typeName, 
expectClass, features) -> {
+            JsonAllowlistManager.getInstance().checkClass(typeName);
+            return null;
+        });
+    }
+
     public static final String NAME = "fastjson";
 
     @Override
@@ -85,8 +97,11 @@ public class FastjsonJsonSerializer implements 
JsonSerializer {
             return null;
         }
         try {
-            return JSON.parseObject(text, type);
+            return JSON.parseObject(text, type, ALLOWLIST_PARSER_CONFIG, 
Feature.SupportAutoType, Feature.OrderedField);
+        } catch (SecurityException e) {
+            throw e;
         } catch (Exception e) {
+            rethrowIfSecurityException(e);
             throw new JsonParseException("FastJSON deserialize error", e);
         }
     }
@@ -132,13 +147,28 @@ public class FastjsonJsonSerializer implements 
JsonSerializer {
             if ("[]".equals(text)) {
                 return (T) new java.util.ArrayList<>();
             }
+
             if (ignoreAutoType) {
                 return JSON.parseObject(text, type, 
READER_FEATURES_IGNORE_AUTO_TYPE);
             } else {
-                return JSON.parseObject(text, type, 
READER_FEATURES_SUPPORT_AUTO_TYPE);
+                return JSON.parseObject(
+                        text, type, ALLOWLIST_PARSER_CONFIG, 
Feature.SupportAutoType, Feature.OrderedField);
             }
+        } catch (SecurityException e) {
+            throw e;
         } catch (Exception e) {
+            rethrowIfSecurityException(e);
             throw new JsonParseException("FastJSON deserialize error", e);
         }
     }
+
+    private static void rethrowIfSecurityException(Throwable e) {
+        Throwable cause = e.getCause();
+        while (cause != null) {
+            if (cause instanceof SecurityException) {
+                throw (SecurityException) cause;
+            }
+            cause = cause.getCause();
+        }
+    }
 }
diff --git 
a/json-common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java
 
b/json-common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java
index 62213320fb..9986532cc1 100644
--- 
a/json-common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java
+++ 
b/json-common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java
@@ -20,10 +20,14 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
+import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
 import org.apache.seata.common.exception.JsonParseException;
+import org.apache.seata.common.json.JsonAllowlistManager;
 import org.apache.seata.common.json.JsonSerializer;
 import org.apache.seata.common.loader.LoadLevel;
 
@@ -52,9 +56,16 @@ public class JacksonJsonSerializer implements JsonSerializer 
{
                 .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
                 .setSerializationInclusion(JsonInclude.Include.NON_NULL);
 
+        AllowlistTypeValidator validator = new AllowlistTypeValidator();
+        ObjectMapper.DefaultTypeResolverBuilder typer =
+                new 
ObjectMapper.DefaultTypeResolverBuilder(DefaultTyping.NON_FINAL, validator);
+        typer.init(JsonTypeInfo.Id.CLASS, null);
+        typer.inclusion(JsonTypeInfo.As.PROPERTY);
+        typer.typeProperty("@type");
+
         this.objectMapperWithAutoType = new ObjectMapper()
                 .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false)
-                .enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, 
"@type")
+                .setDefaultTyping(typer)
                 .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
                 .setSerializationInclusion(JsonInclude.Include.NON_NULL);
 
@@ -83,7 +94,7 @@ public class JacksonJsonSerializer implements JsonSerializer {
             return null;
         }
         try {
-            return mapper.readValue(text, clazz);
+            return defaultObjectMapper.readValue(text, clazz);
         } catch (IOException e) {
             throw new JsonParseException("Jackson deserialize error", e);
         }
@@ -96,7 +107,10 @@ public class JacksonJsonSerializer implements 
JsonSerializer {
         }
         try {
             return objectMapperWithAutoType.readValue(text, 
objectMapperWithAutoType.constructType(type));
+        } catch (SecurityException e) {
+            throw e;
         } catch (IOException e) {
+            rethrowIfSecurityException(e);
             throw new JsonParseException("Jackson deserialize error", e);
         }
     }
@@ -152,8 +166,43 @@ public class JacksonJsonSerializer implements 
JsonSerializer {
             } else {
                 return objectMapperWithAutoType.readValue(json, type);
             }
+        } catch (SecurityException e) {
+            throw e;
         } catch (IOException e) {
+            rethrowIfSecurityException(e);
             throw new JsonParseException("Jackson deserialize error", e);
         }
     }
+
+    private static void rethrowIfSecurityException(Throwable e) {
+        Throwable cause = e.getCause();
+        while (cause != null) {
+            if (cause instanceof SecurityException) {
+                throw (SecurityException) cause;
+            }
+            cause = cause.getCause();
+        }
+    }
+
+    private static class AllowlistTypeValidator extends 
PolymorphicTypeValidator.Base {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public Validity validateBaseType(MapperConfig<?> config, JavaType 
baseType) {
+            return Validity.INDETERMINATE;
+        }
+
+        @Override
+        public Validity validateSubClassName(MapperConfig<?> config, JavaType 
baseType, String subClassName) {
+            // Throws SecurityException if not allowed
+            JsonAllowlistManager.getInstance().checkClass(subClassName);
+            return Validity.ALLOWED;
+        }
+
+        @Override
+        public Validity validateSubType(MapperConfig<?> config, JavaType 
baseType, JavaType subType) {
+            
JsonAllowlistManager.getInstance().checkClass(subType.getRawClass().getName());
+            return Validity.ALLOWED;
+        }
+    }
 }
diff --git 
a/json-common/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java
 
b/json-common/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java
new file mode 100644
index 0000000000..fc792c4ad0
--- /dev/null
+++ 
b/json-common/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.common.json;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for FastJSON serializer with allowlist security check
+ */
+public class FastjsonAllowlistTest {
+
+    private JsonSerializer jsonSerializer;
+
+    @BeforeEach
+    void setUp() {
+        jsonSerializer = JsonSerializerFactory.getSerializer("fastjson");
+    }
+
+    @AfterEach
+    void tearDown() {
+        JsonAllowlistManager.getInstance().clearUserAllowlist();
+    }
+
+    @Test
+    public void testParseObject_allowedSeataClass() {
+
+        String json =
+                
"{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$AllowedTestClass\",\"name\":\"test\"}";
+
+        AllowedTestClass result = jsonSerializer.parseObject(json, 
AllowedTestClass.class, false);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("test");
+    }
+
+    @Test
+    public void testParseObject_allowedJavaClass() {
+
+        String json = "{\"@type\":\"java.util.HashMap\"}";
+
+        Object result = jsonSerializer.parseObject(json, Object.class, false);
+
+        assertThat(result).isNotNull();
+    }
+
+    @Test
+    public void testParseObject_notAllowedClass() {
+
+        String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm 
-rf /\"}";
+
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json, 
Object.class, false))
+                .isInstanceOf(SecurityException.class)
+                .hasMessageContaining("not in JSON deserialization allowlist")
+                .hasMessageContaining("com.malicious.EvilClass");
+    }
+
+    @Test
+    public void testParseObject_userAllowedClass() {
+
+        
JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass");
+
+        String json = 
"{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}";
+
+        Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, 
Object.class, false));
+    }
+
+    @Test
+    public void testParseObject_userAllowedPrefix() {
+        
JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model.");
+
+        String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}";
+
+        Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, 
Object.class, false));
+    }
+
+    @Test
+    public void testParseObject_ignoreAutoType_bypasses_check() {
+
+        String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm 
-rf /\"}";
+
+        try {
+            jsonSerializer.parseObject(json, Object.class, true);
+        } catch (SecurityException e) {
+            throw new AssertionError("Should not throw SecurityException when 
ignoreAutoType=true", e);
+        } catch (Exception e) {
+
+            assertThat(e).isNotInstanceOf(SecurityException.class);
+        }
+    }
+
+    @Test
+    public void testParseObject_noAutoType_bypasses_check() {
+
+        String json = "{\"name\":\"test\",\"value\":123}";
+
+        TestObject result = jsonSerializer.parseObject(json, TestObject.class, 
false);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("test");
+    }
+
+    @Test
+    public void testParseObject_multipleAutoTypes() {
+
+        String json = 
"{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$ContainerClass\","
+                + 
"\"inner\":{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}";
+
+        ContainerClass result = jsonSerializer.parseObject(json, 
ContainerClass.class, false);
+
+        assertThat(result).isNotNull();
+    }
+
+    @Test
+    public void testParseObject_multipleAutoTypes_oneNotAllowed() {
+
+        String json = 
"{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$ContainerClass\","
+                + 
"\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}";
+
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json, 
ContainerClass.class, false))
+                .isInstanceOf(SecurityException.class)
+                .hasMessageContaining("com.malicious.EvilClass");
+    }
+
+    @Test
+    public void testParseObject_atTypeInStringValue_notBlocked() {
+        // @type appearing inside a string value should not be treated as 
AutoType metadata
+        String json = "{\"description\":\"the \\\"@type\\\" field is 
important\",\"name\":\"test\"}";
+
+        TestObject result = jsonSerializer.parseObject(json, TestObject.class, 
false);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("test");
+    }
+
+    @Test
+    public void testLoadUserAllowlist_thenParse() {
+        
JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO");
+
+        String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}";
+        try {
+            jsonSerializer.parseObject(json1, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+
+        }
+
+        String json2 = 
"{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}";
+        try {
+            jsonSerializer.parseObject(json2, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+
+        }
+
+        String json3 = 
"{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}";
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json3, 
Object.class, false))
+                .isInstanceOf(SecurityException.class);
+    }
+
+    public static class TestObject {
+        private String name;
+        private int value;
+
+        public TestObject() {}
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public void setValue(int value) {
+            this.value = value;
+        }
+    }
+
+    public static class AllowedTestClass {
+        private String name;
+
+        public AllowedTestClass() {}
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
+
+    public static class ContainerClass {
+        private AllowedTestClass inner;
+
+        public ContainerClass() {}
+
+        public AllowedTestClass getInner() {
+            return inner;
+        }
+
+        public void setInner(AllowedTestClass inner) {
+            this.inner = inner;
+        }
+    }
+}
diff --git 
a/json-common/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java
 
b/json-common/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java
new file mode 100644
index 0000000000..522b301a08
--- /dev/null
+++ 
b/json-common/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.common.json;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for Jackson serializer with allowlist security check
+ */
+public class JacksonAllowlistTest {
+
+    private JsonSerializer jsonSerializer;
+
+    @BeforeEach
+    void setUp() {
+        jsonSerializer = JsonSerializerFactory.getSerializer("jackson");
+    }
+
+    @AfterEach
+    void tearDown() {
+        JsonAllowlistManager.getInstance().clearUserAllowlist();
+    }
+
+    @Test
+    public void testParseObject_allowedSeataClass() {
+
+        String json =
+                
"{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$AllowedTestClass\",\"name\":\"test\"}";
+
+        AllowedTestClass result = jsonSerializer.parseObject(json, 
AllowedTestClass.class, false);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("test");
+    }
+
+    @Test
+    public void testParseObject_allowedJavaClass() {
+
+        String json = "{\"@type\":\"java.util.HashMap\"}";
+
+        Object result = jsonSerializer.parseObject(json, Object.class, false);
+
+        assertThat(result).isNotNull();
+    }
+
+    @Test
+    public void testParseObject_notAllowedClass() {
+
+        String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm 
-rf /\"}";
+
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json, 
Object.class, false))
+                .isInstanceOf(SecurityException.class)
+                .hasMessageContaining("not in JSON deserialization allowlist")
+                .hasMessageContaining("com.malicious.EvilClass");
+    }
+
+    @Test
+    public void testParseObject_userAllowedClass() {
+
+        
JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass");
+
+        String json = 
"{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}";
+
+        try {
+            jsonSerializer.parseObject(json, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+            assertThat(e).isNotInstanceOf(SecurityException.class);
+        }
+    }
+
+    @Test
+    public void testParseObject_userAllowedPrefix() {
+        
JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model.");
+
+        String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}";
+
+        try {
+            jsonSerializer.parseObject(json, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+
+            assertThat(e).isNotInstanceOf(SecurityException.class);
+        }
+    }
+
+    @Test
+    public void testParseObject_ignoreAutoType_bypasses_check() {
+
+        String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm 
-rf /\"}";
+
+        try {
+            jsonSerializer.parseObject(json, Object.class, true);
+        } catch (SecurityException e) {
+            throw new AssertionError("Should not throw SecurityException when 
ignoreAutoType=true", e);
+        } catch (Exception e) {
+
+            assertThat(e).isNotInstanceOf(SecurityException.class);
+        }
+    }
+
+    @Test
+    public void testParseObject_noAutoType_bypasses_check() {
+        // Jackson's objectMapperWithAutoType requires @type for non-final 
types,
+        // so use the 2-arg parseObject (defaultObjectMapper, no AutoType) to 
verify
+        // that normal JSON without @type does not trigger any 
SecurityException.
+        String json = "{\"name\":\"test\",\"value\":123}";
+
+        TestObject result = jsonSerializer.parseObject(json, TestObject.class);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("test");
+    }
+
+    @Test
+    public void testParseObject_multipleAutoTypes() {
+
+        String json = 
"{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$ContainerClass\","
+                + 
"\"inner\":{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}";
+
+        ContainerClass result = jsonSerializer.parseObject(json, 
ContainerClass.class, false);
+
+        assertThat(result).isNotNull();
+    }
+
+    @Test
+    public void testParseObject_multipleAutoTypes_oneNotAllowed() {
+
+        String json = 
"{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$ContainerClass\","
+                + 
"\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}";
+
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json, 
ContainerClass.class, false))
+                .isInstanceOf(SecurityException.class)
+                .hasMessageContaining("com.malicious.EvilClass");
+    }
+
+    @Test
+    public void testParseObject_atTypeInStringValue_notBlocked() {
+        // @type appearing inside a string value should not be treated as 
AutoType metadata.
+        // The JSON has a real @type for the root object (required by 
Jackson's DefaultTyping),
+        // and a fake @type inside a string value which should not be flagged.
+        String json = 
"{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$TestObject\","
+                + "\"name\":\"the \\\"@type\\\" field is 
important\",\"value\":123}";
+
+        TestObject result = jsonSerializer.parseObject(json, TestObject.class, 
false);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("the \"@type\" field is 
important");
+    }
+
+    @Test
+    public void testLoadUserAllowlist_thenParse() {
+        
JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO");
+
+        String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}";
+        try {
+            jsonSerializer.parseObject(json1, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+
+        }
+
+        String json2 = 
"{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}";
+        try {
+            jsonSerializer.parseObject(json2, Object.class, false);
+        } catch (SecurityException e) {
+            throw e;
+        } catch (Exception e) {
+
+        }
+
+        String json3 = 
"{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}";
+        assertThatThrownBy(() -> jsonSerializer.parseObject(json3, 
Object.class, false))
+                .isInstanceOf(SecurityException.class);
+    }
+
+    public static class TestObject {
+        private String name;
+        private int value;
+
+        public TestObject() {}
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public void setValue(int value) {
+            this.value = value;
+        }
+    }
+
+    public static class AllowedTestClass {
+        private String name;
+
+        public AllowedTestClass() {}
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
+
+    public static class ContainerClass {
+        private AllowedTestClass inner;
+
+        public ContainerClass() {}
+
+        public AllowedTestClass getInner() {
+            return inner;
+        }
+
+        public void setInner(AllowedTestClass inner) {
+            this.inner = inner;
+        }
+    }
+}
diff --git 
a/json-common/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java
 
b/json-common/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java
new file mode 100644
index 0000000000..c33425308b
--- /dev/null
+++ 
b/json-common/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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.common.json;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link JsonAllowlistManager}
+ */
+public class JsonAllowlistManagerTest {
+
+    @AfterEach
+    void tearDown() {
+
+        JsonAllowlistManager.getInstance().clearUserAllowlist();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_primitiveWrappers() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThat(manager.isAllowed("java.lang.String")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Integer")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Long")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Boolean")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Double")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Float")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Byte")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Short")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Character")).isTrue();
+        assertThat(manager.isAllowed("java.lang.Number")).isTrue();
+        assertThat(manager.isAllowed("java.math.BigDecimal")).isTrue();
+        assertThat(manager.isAllowed("java.math.BigInteger")).isTrue();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_arrays() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        // 1D primitive arrays
+        assertThat(manager.isAllowed("[B")).isTrue();
+        assertThat(manager.isAllowed("[I")).isTrue();
+        assertThat(manager.isAllowed("[J")).isTrue();
+        assertThat(manager.isAllowed("[Z")).isTrue();
+        assertThat(manager.isAllowed("[C")).isTrue();
+        assertThat(manager.isAllowed("[S")).isTrue();
+        assertThat(manager.isAllowed("[F")).isTrue();
+        assertThat(manager.isAllowed("[D")).isTrue();
+
+        // Multi-dimensional primitive arrays
+        assertThat(manager.isAllowed("[[I")).isTrue();
+        assertThat(manager.isAllowed("[[Z")).isTrue();
+        assertThat(manager.isAllowed("[[B")).isTrue();
+        assertThat(manager.isAllowed("[[J")).isTrue();
+        assertThat(manager.isAllowed("[[[D")).isTrue();
+        assertThat(manager.isAllowed("[[[F")).isTrue();
+
+        // Object arrays
+        assertThat(manager.isAllowed("[Ljava.lang.String;")).isTrue();
+        assertThat(manager.isAllowed("[Ljava.lang.Object;")).isTrue();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_dateTime() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThat(manager.isAllowed("java.util.Date")).isTrue();
+        assertThat(manager.isAllowed("java.sql.Timestamp")).isTrue();
+        assertThat(manager.isAllowed("java.time.LocalDateTime")).isTrue();
+        assertThat(manager.isAllowed("java.time.LocalDate")).isTrue();
+        assertThat(manager.isAllowed("java.time.Instant")).isTrue();
+        assertThat(manager.isAllowed("java.time.ZonedDateTime")).isTrue();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_collections() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThat(manager.isAllowed("java.util.ArrayList")).isTrue();
+        assertThat(manager.isAllowed("java.util.LinkedList")).isTrue();
+        assertThat(manager.isAllowed("java.util.HashMap")).isTrue();
+        assertThat(manager.isAllowed("java.util.LinkedHashMap")).isTrue();
+        assertThat(manager.isAllowed("java.util.HashSet")).isTrue();
+        assertThat(manager.isAllowed("java.util.TreeMap")).isTrue();
+        
assertThat(manager.isAllowed("java.util.concurrent.ConcurrentHashMap")).isTrue();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_seataPrefix() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
assertThat(manager.isAllowed("org.apache.seata.core.model.BranchType")).isTrue();
+        
assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People"))
+                .isTrue();
+        
assertThat(manager.isAllowed("org.apache.seata.rm.datasource.undo.UndoLogParser"))
+                .isTrue();
+        
assertThat(manager.isAllowed("org.apache.seata.common.json.JsonAllowlistManager"))
+                .isTrue();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_objectArrayDescriptorsByPrefix() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
assertThat(manager.isAllowed("[Lorg.apache.seata.common.json.JsonAllowlistManager;"))
+                .isTrue();
+        
assertThat(manager.isAllowed("[Lio.seata.saga.engine.mock.DemoService$People;"))
+                .isTrue();
+        
assertThat(manager.isAllowed("[[Lio.seata.saga.engine.mock.DemoService$People;"))
+                .isTrue();
+        assertThat(manager.isAllowed("[Lcom.malicious.EvilClass;")).isFalse();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_objectArrayCanonicalNameByPrefix() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People[]"))
+                .isTrue();
+        
assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People[][]"))
+                .isTrue();
+        assertThat(manager.isAllowed("com.malicious.EvilClass[]")).isFalse();
+    }
+
+    @Test
+    public void testBuiltinAllowlist_notAllowed() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
assertThat(manager.isAllowed("com.sun.rowset.JdbcRowSetImpl")).isFalse();
+        assertThat(manager.isAllowed("java.lang.Runtime")).isFalse();
+        assertThat(manager.isAllowed("java.lang.ProcessBuilder")).isFalse();
+        assertThat(manager.isAllowed("javax.naming.InitialContext")).isFalse();
+        assertThat(manager.isAllowed("com.example.MaliciousClass")).isFalse();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_exactMatch() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
manager.loadUserAllowlist("com.example.MyClass,com.example.AnotherClass");
+
+        assertThat(manager.isAllowed("com.example.MyClass")).isTrue();
+        assertThat(manager.isAllowed("com.example.AnotherClass")).isTrue();
+        assertThat(manager.isAllowed("com.example.NotInList")).isFalse();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_prefixMatch() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist("com.company.model.,com.company.dto.");
+
+        assertThat(manager.isAllowed("com.company.model.User")).isTrue();
+        assertThat(manager.isAllowed("com.company.model.Order")).isTrue();
+        assertThat(manager.isAllowed("com.company.dto.UserDTO")).isTrue();
+        
assertThat(manager.isAllowed("com.company.service.UserService")).isFalse();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_mixedMatch() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        
manager.loadUserAllowlist("com.company.model.,com.thirdparty.SpecificClass");
+
+        assertThat(manager.isAllowed("com.company.model.User")).isTrue();
+        
assertThat(manager.isAllowed("com.company.model.sub.NestedClass")).isTrue();
+        assertThat(manager.isAllowed("com.thirdparty.SpecificClass")).isTrue();
+        assertThat(manager.isAllowed("com.thirdparty.OtherClass")).isFalse();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_withSpaces() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist("  com.example.Class1  ,  com.example.Class2 
 ");
+
+        assertThat(manager.isAllowed("com.example.Class1")).isTrue();
+        assertThat(manager.isAllowed("com.example.Class2")).isTrue();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_emptyEntries() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist("com.example.Class1,,com.example.Class2,");
+
+        assertThat(manager.isAllowed("com.example.Class1")).isTrue();
+        assertThat(manager.isAllowed("com.example.Class2")).isTrue();
+    }
+
+    @Test
+    public void testLoadUserAllowlist_nullOrEmpty() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist(null);
+        manager.loadUserAllowlist("");
+
+        assertThat(manager.isAllowed("java.lang.String")).isTrue();
+    }
+
+    @Test
+    public void testAddUserClass() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.addUserClass("com.example.DynamicClass");
+
+        assertThat(manager.isAllowed("com.example.DynamicClass")).isTrue();
+    }
+
+    @Test
+    public void testAddUserPrefix() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.addUserPrefix("com.dynamic.pkg.");
+
+        assertThat(manager.isAllowed("com.dynamic.pkg.Class1")).isTrue();
+        assertThat(manager.isAllowed("com.dynamic.pkg.sub.Class2")).isTrue();
+    }
+
+    @Test
+    public void testClearUserAllowlist() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist("com.example.ToBeCleared");
+        assertThat(manager.isAllowed("com.example.ToBeCleared")).isTrue();
+
+        manager.clearUserAllowlist();
+        assertThat(manager.isAllowed("com.example.ToBeCleared")).isFalse();
+
+        assertThat(manager.isAllowed("java.lang.String")).isTrue();
+    }
+
+    @Test
+    public void testCheckClass_allowed() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.checkClass("java.lang.String");
+        manager.checkClass("java.util.ArrayList");
+        manager.checkClass("org.apache.seata.core.model.BranchType");
+    }
+
+    @Test
+    public void testCheckClass_notAllowed() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThatThrownBy(() -> manager.checkClass("com.malicious.EvilClass"))
+                .isInstanceOf(SecurityException.class)
+                .hasMessageContaining("not in JSON deserialization allowlist")
+                .hasMessageContaining("com.malicious.EvilClass")
+                .hasMessageContaining("seata.json.allowlist");
+    }
+
+    @Test
+    public void testCheckClass_userAllowed() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.loadUserAllowlist("com.myapp.model.");
+
+        manager.checkClass("com.myapp.model.User");
+    }
+
+    @Test
+    public void testIsAllowed_null() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThat(manager.isAllowed(null)).isFalse();
+    }
+
+    @Test
+    public void testCheckClass_null() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        assertThatThrownBy(() -> 
manager.checkClass(null)).isInstanceOf(SecurityException.class);
+    }
+
+    @Test
+    public void testAddUserClass_nullOrEmpty() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.addUserClass(null);
+        manager.addUserClass("");
+    }
+
+    @Test
+    public void testAddUserPrefix_nullOrEmpty() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.addUserPrefix(null);
+        manager.addUserPrefix("");
+    }
+
+    @Test
+    public void testSingleton() {
+        JsonAllowlistManager instance1 = JsonAllowlistManager.getInstance();
+        JsonAllowlistManager instance2 = JsonAllowlistManager.getInstance();
+
+        assertThat(instance1).isSameAs(instance2);
+    }
+
+    @Test
+    public void testCacheWorks() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        boolean result1 = manager.isAllowed("java.lang.String");
+
+        boolean result2 = manager.isAllowed("java.lang.String");
+
+        assertThat(result1).isTrue();
+        assertThat(result2).isTrue();
+    }
+
+    @Test
+    public void testCacheClearedOnLoadUserAllowlist() {
+        JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
+
+        manager.isAllowed("com.example.CacheTest");
+
+        manager.loadUserAllowlist("com.example.CacheTest");
+
+        assertThat(manager.isAllowed("com.example.CacheTest")).isTrue();
+    }
+}
diff --git a/script/client/spring/application.properties 
b/script/client/spring/application.properties
index 804b0d09e2..dcac5bac1e 100755
--- a/script/client/spring/application.properties
+++ b/script/client/spring/application.properties
@@ -183,6 +183,8 @@ seata.registry.zk.password=
 
 seata.registry.custom.name=
 
+seata.json.allowlist=
+
 seata.tcc.fence.log-table-name=tcc_fence_log
 seata.tcc.fence.clean-period=1h
 #You can choose from the following options: fastjson, jackson, gson
diff --git a/script/client/spring/application.yml 
b/script/client/spring/application.yml
index 67e17de611..2c549f5b06 100755
--- a/script/client/spring/application.yml
+++ b/script/client/spring/application.yml
@@ -199,6 +199,8 @@ seata:
       name:
   log:
     exception-rate: 100
+  json:
+    allowlist: 
   tcc:
     fence:
       log-table-name: tcc_fence_log
diff --git 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml
index 1f5e7ca9ce..858ae42537 100644
--- a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml
+++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml
@@ -37,5 +37,14 @@
             <artifactId>seata-spring-autoconfigure-core</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.seata</groupId>
+            <artifactId>json-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java
 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java
index ae1fcb759a..7fa8314c1c 100644
--- 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java
+++ 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java
@@ -20,6 +20,7 @@ import org.apache.seata.common.holder.ObjectHolder;
 import org.apache.seata.rm.fence.SpringFenceConfig;
 import org.apache.seata.saga.engine.StateMachineConfig;
 import 
org.apache.seata.spring.boot.autoconfigure.properties.SagaAsyncThreadPoolProperties;
+import 
org.apache.seata.spring.boot.autoconfigure.properties.SeataJsonProperties;
 import org.apache.seata.spring.boot.autoconfigure.properties.SeataProperties;
 import 
org.apache.seata.spring.boot.autoconfigure.properties.SeataTccProperties;
 import 
org.apache.seata.spring.boot.autoconfigure.properties.client.LoadBalanceProperties;
@@ -38,6 +39,7 @@ import static 
org.apache.seata.common.Constants.OBJECT_KEY_SPRING_CONFIGURABLE_E
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.CLIENT_RM_PREFIX;
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.CLIENT_TM_PREFIX;
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.COMPRESS_PREFIX;
+import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.JSON_PREFIX;
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.LOAD_BALANCE_PREFIX;
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.LOCK_PREFIX;
 import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.PROPERTY_BEAN_MAP;
@@ -67,6 +69,7 @@ public class SeataClientEnvironmentPostProcessor implements 
EnvironmentPostProce
         PROPERTY_BEAN_MAP.put(SAGA_STATE_MACHINE_PREFIX, 
StateMachineConfig.class);
         PROPERTY_BEAN_MAP.put(SAGA_ASYNC_THREAD_POOL_PREFIX, 
SagaAsyncThreadPoolProperties.class);
         PROPERTY_BEAN_MAP.put(TCC_PREFIX, SeataTccProperties.class);
+        PROPERTY_BEAN_MAP.put(JSON_PREFIX, SeataJsonProperties.class);
     }
 
     @Override
diff --git 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonProperties.java
 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonProperties.java
new file mode 100644
index 0000000000..d7cc242de4
--- /dev/null
+++ 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonProperties.java
@@ -0,0 +1,54 @@
+/*
+ * 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.spring.boot.autoconfigure.properties;
+
+import org.apache.seata.common.json.JsonAllowlistManager;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+
+import static 
org.apache.seata.spring.boot.autoconfigure.StarterConstants.JSON_PREFIX;
+
+/**
+ * Seata JSON configuration properties
+ */
+@Component
+@ConfigurationProperties(prefix = JSON_PREFIX)
+public class SeataJsonProperties {
+
+    /**
+     * JSON deserialization allowlist, comma-separated
+     * Entries ending with '.' are prefix matches, otherwise exact matches
+     * Example: com.company.model.,com.company.dto.,com.company.SomeClass
+     */
+    private String allowlist;
+
+    public String getAllowlist() {
+        return allowlist;
+    }
+
+    public SeataJsonProperties setAllowlist(String allowlist) {
+        this.allowlist = allowlist;
+        return this;
+    }
+
+    @PostConstruct
+    public void init() {
+        JsonAllowlistManager.getInstance().loadUserAllowlist(allowlist);
+    }
+}
diff --git 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonPropertiesTest.java
 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonPropertiesTest.java
new file mode 100644
index 0000000000..5ff2c27c8d
--- /dev/null
+++ 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/SeataJsonPropertiesTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.spring.boot.autoconfigure.properties;
+
+import org.apache.seata.common.json.JsonAllowlistManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SeataJsonPropertiesTest {
+
+    @AfterEach
+    public void cleanup() {
+        JsonAllowlistManager.getInstance().clearUserAllowlist();
+    }
+
+    @Test
+    public void testSeataJsonProperties() {
+        SeataJsonProperties props = new SeataJsonProperties();
+
+        String allowlist = 
"com.test.model.,com.test.dto.,com.test.SpecialBean";
+        props.setAllowlist(allowlist);
+
+        Assertions.assertEquals(allowlist, props.getAllowlist());
+    }
+
+    @Test
+    public void testAllowlistAppliedToManager() {
+        SeataJsonProperties props = new SeataJsonProperties();
+        
props.setAllowlist("com.test.model.,com.test.dto.,com.test.SpecialBean");
+        props.init();
+
+        // Prefix match
+        
Assertions.assertTrue(JsonAllowlistManager.getInstance().isAllowed("com.test.model.User"));
+        
Assertions.assertTrue(JsonAllowlistManager.getInstance().isAllowed("com.test.dto.OrderDto"));
+
+        // Exact match
+        
Assertions.assertTrue(JsonAllowlistManager.getInstance().isAllowed("com.test.SpecialBean"));
+
+        // Not allowed
+        
Assertions.assertFalse(JsonAllowlistManager.getInstance().isAllowed("com.test.SpecialBeanExtra"));
+        
Assertions.assertFalse(JsonAllowlistManager.getInstance().isAllowed("com.unknown.MaliciousClass"));
+    }
+
+    @Test
+    public void testEmptyAllowlist() {
+        SeataJsonProperties props = new SeataJsonProperties();
+        props.setAllowlist("");
+
+        Assertions.assertEquals("", props.getAllowlist());
+    }
+
+    @Test
+    public void testNullAllowlist() {
+        SeataJsonProperties props = new SeataJsonProperties();
+        props.setAllowlist(null);
+
+        Assertions.assertNull(props.getAllowlist());
+    }
+}
diff --git 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
index 64f9c395f9..1d18cc03c0 100644
--- 
a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
+++ 
b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java
@@ -40,6 +40,7 @@ public interface StarterConstants {
     String LOG_PREFIX = SEATA_PREFIX + ".log";
     String COMPRESS_PREFIX = UNDO_PREFIX + ".compress";
     String TCC_PREFIX = SEATA_PREFIX + ".tcc";
+    String JSON_PREFIX = SEATA_PREFIX + ".json";
     String TCC_FENCE_PREFIX = TCC_PREFIX + ".fence";
     String SAGA_STATE_MACHINE_PREFIX = SAGA_PREFIX + ".state-machine";
     String SAGA_ASYNC_THREAD_POOL_PREFIX = SAGA_STATE_MACHINE_PREFIX + 
".async-thread-pool";


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

Reply via email to