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]