Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/en-us/2.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#8109](https://github.com/apache/incubator-seata/pull/8109)] reduce npmjs dependencies in saga module
- [[#8112](https://github.com/apache/incubator-seata/pull/8112)] optimize seata-server test performance
- [[#8116](https://github.com/apache/incubator-seata/pull/8116)] fix incompatible dependencies
- [[#8120](https://github.com/apache/incubator-seata/pull/8120)] limit allowlist cache growth

### security:

Expand Down
1 change: 1 addition & 0 deletions changes/zh-cn/2.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
- [[#8109](https://github.com/apache/incubator-seata/pull/8109)] 精简 saga 模块 npmjs 依赖
- [[#8112](https://github.com/apache/incubator-seata/pull/8112)] 优化 seata-server 测试性能
- [[#8116](https://github.com/apache/incubator-seata/pull/8116)] 修复不兼容的依赖
- [[#8120](https://github.com/apache/incubator-seata/pull/8120)] 限制了json-common白名单缓存

### security:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public class JsonAllowlistManager {

private static final JsonAllowlistManager INSTANCE = new JsonAllowlistManager();

private static final int MAX_CLASS_NAME_LENGTH = 1024;

private static final int MAX_REPORTED_CLASS_NAME_LENGTH = 256;

private static final int MAX_CACHE_SIZE = 4096;

/**
* Built-in exact match allowlist
*/
Expand Down Expand Up @@ -107,19 +113,27 @@ public void addUserPrefix(String prefix) {
* Check if a class is allowed for deserialization
*/
public boolean isAllowed(String className) {
if (className == null) {
if (className == null || className.length() > MAX_CLASS_NAME_LENGTH) {
return false;
}
return cache.computeIfAbsent(className, this::doCheck);
if (cache.get(className) != null) {
Comment thread
LegendPei marked this conversation as resolved.
return true;
}

boolean allowed = doCheck(className);
if (allowed) {
cacheAllowedClass(className);
}
return allowed;
}

/**
* 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.");
throw new SecurityException("Class not in JSON deserialization allowlist: "
+ formatClassNameForMessage(className) + ". Please add it to seata.json.allowlist configuration.");
}
}

Expand All @@ -143,6 +157,24 @@ private boolean doCheck(String className) {
return componentClassName != null && isExactOrPrefixAllowed(componentClassName);
}

private void cacheAllowedClass(String className) {
synchronized (cache) {
if (cache.size() < MAX_CACHE_SIZE) {
cache.put(className, Boolean.TRUE);
}
}
}

private String formatClassNameForMessage(String className) {
if (className == null || className.length() <= MAX_REPORTED_CLASS_NAME_LENGTH) {
return String.valueOf(className);
}
return className.substring(0, MAX_REPORTED_CLASS_NAME_LENGTH)
+ "...(truncated, length="
+ className.length()
+ ")";
}

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Field;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

Expand Down Expand Up @@ -268,6 +271,18 @@ public void testCheckClass_notAllowed() {
.hasMessageContaining("seata.json.allowlist");
}

@Test
public void testCheckClass_overlongNameIsTruncatedInMessage() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
String className = buildClassName(1100);

assertThatThrownBy(() -> manager.checkClass(className))
.isInstanceOf(SecurityException.class)
.hasMessageContaining("not in JSON deserialization allowlist")
.hasMessageContaining("truncated, length=1100")
.hasMessageNotContaining(className);
}

@Test
public void testCheckClass_userAllowed() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
Expand Down Expand Up @@ -318,13 +333,51 @@ public void testSingleton() {
@Test
public void testCacheWorks() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
manager.clearUserAllowlist();

boolean result1 = manager.isAllowed("java.lang.String");

boolean result2 = manager.isAllowed("java.lang.String");

assertThat(result1).isTrue();
assertThat(result2).isTrue();
assertThat(cacheSize(manager)).isEqualTo(1);
}

@Test
public void testRejectedClassesAreNotCached() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
manager.clearUserAllowlist();

for (int i = 0; i < 100; i++) {
assertThat(manager.isAllowed("com.malicious.EvilClass" + i)).isFalse();
}

assertThat(cacheSize(manager)).isZero();
}

@Test
public void testOverlongClassNameIsRejectedAndNotCached() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
manager.clearUserAllowlist();
String className = buildClassName(1100);

assertThat(manager.isAllowed(className)).isFalse();

assertThat(cacheSize(manager)).isZero();
}

@Test
public void testAllowedClassCacheIsBounded() {
JsonAllowlistManager manager = JsonAllowlistManager.getInstance();
manager.clearUserAllowlist();

for (int i = 0; i < 5000; i++) {
assertThat(manager.isAllowed("org.apache.seata.generated.AllowedClass" + i))
.isTrue();
}

assertThat(cacheSize(manager)).isEqualTo(4096);
}

@Test
Expand All @@ -337,4 +390,26 @@ public void testCacheClearedOnLoadUserAllowlist() {

assertThat(manager.isAllowed("com.example.CacheTest")).isTrue();
}

@SuppressWarnings("unchecked")
private static int cacheSize(JsonAllowlistManager manager) {
try {
Field cacheField = JsonAllowlistManager.class.getDeclaredField("cache");
cacheField.setAccessible(true);
Map<String, Boolean> cache = (Map<String, Boolean>) cacheField.get(manager);
return cache.size();
} catch (ReflectiveOperationException e) {
throw new AssertionError("Failed to read JsonAllowlistManager cache", e);
}
}

private static String buildClassName(int length) {
String prefix = "com.example.";
StringBuilder builder = new StringBuilder(length);
builder.append(prefix);
while (builder.length() < length) {
builder.append('A');
}
return builder.toString();
}
}
Loading