This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new f6a633e12 feat(java): add json type checker (#3829)
f6a633e12 is described below
commit f6a633e12ba42767a60a34e26a4c12e4df122254
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Jul 9 11:31:57 2026 +0530
feat(java): add json type checker (#3829)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../org/apache/fory/resolver/DisallowedList.java | 13 +-
.../apache/fory/resolver/DisallowedListTest.java | 7 +
.../java/org/apache/fory/json/ForyJsonBuilder.java | 15 +-
.../main/java/org/apache/fory/json/JsonConfig.java | 31 +-
.../org/apache/fory/json/JsonTypeCheckContext.java | 25 ++
.../java/org/apache/fory/json/JsonTypeChecker.java | 46 +++
.../apache/fory/json/codec/BaseObjectCodec.java | 9 +-
.../apache/fory/json/codec/CollectionCodec.java | 1 +
.../java/org/apache/fory/json/codec/MapCodec.java | 2 +
.../apache/fory/json/resolver/CodecRegistry.java | 10 +
.../fory/json/resolver/JsonSharedRegistry.java | 109 +++++++
.../fory/json/resolver/JsonTypeResolver.java | 7 +
.../java/org/apache/fory/json/JsonScalarTest.java | 10 +-
.../org/apache/fory/json/JsonTypeCheckerTest.java | 317 +++++++++++++++++++++
14 files changed, 581 insertions(+), 21 deletions(-)
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/DisallowedList.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/DisallowedList.java
index 4b1254020..88a4e03a1 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/DisallowedList.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/DisallowedList.java
@@ -20,12 +20,13 @@
package org.apache.fory.resolver;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.fory.exception.InsecureException;
/** A class to record which classes are not allowed for serialization. */
-class DisallowedList {
+public final class DisallowedList {
// Embedded disallowed class names list - no external file dependency
private static final String[] DISALLOWED_CLASSES = {
@@ -290,14 +291,16 @@ class DisallowedList {
};
private static final Set<String> DEFAULT_DISALLOWED_LIST_SET =
- Arrays.stream(DISALLOWED_CLASSES).collect(Collectors.toSet());
+
Collections.unmodifiableSet(Arrays.stream(DISALLOWED_CLASSES).collect(Collectors.toSet()));
+
+ private DisallowedList() {}
/**
- * Get the disallowed class names as a Set for testing purposes.
+ * Get the disallowed class names as an immutable set.
*
* @return Set of disallowed class names
*/
- static Set<String> getDisallowedClasses() {
+ public static Set<String> getDisallowedClasses() {
return DEFAULT_DISALLOWED_LIST_SET;
}
@@ -309,7 +312,7 @@ class DisallowedList {
* @param clsName Class Name that needs to be judged.
* @throws InsecureException If the class is in the disallowed list.
*/
- static void checkNotInDisallowedList(String clsName) {
+ public static void checkNotInDisallowedList(String clsName) {
if (DEFAULT_DISALLOWED_LIST_SET.contains(clsName)) {
throw new InsecureException(String.format("%s hit disallowed list",
clsName));
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java
b/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java
index 8425970c0..f15b4297e 100644
---
a/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/resolver/DisallowedListTest.java
@@ -51,6 +51,13 @@ public class DisallowedListTest extends ForyTestBase {
Assert.assertTrue(disallowedClasses.contains("org.apache.xalan.xsltc.trax.TemplatesImpl"));
}
+ @Test
+ public void testDisallowedListImmutable() {
+ Set<String> disallowedClasses = DisallowedList.getDisallowedClasses();
+ Assert.assertThrows(
+ UnsupportedOperationException.class, () ->
disallowedClasses.add("example.NotAllowed"));
+ }
+
@Test
public void testCheckHitDisallowedList() {
// Hit the disallowed list.
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
index a3795048a..1afaafbe3 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
@@ -29,6 +29,7 @@ public final class ForyJsonBuilder {
private boolean asyncCompilationEnabled = true;
private boolean propertyDiscoveryEnabled = true;
private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH;
+ private JsonTypeChecker typeChecker;
private final CodecRegistry codecRegistry = new CodecRegistry();
ForyJsonBuilder() {}
@@ -76,6 +77,17 @@ public final class ForyJsonBuilder {
return this;
}
+ /**
+ * Sets the JSON type checker. Pass {@code null} to allow all non-disallowed
classes.
+ *
+ * <p>The checker must be thread-safe because one {@link ForyJson} instance
can be used
+ * concurrently.
+ */
+ public ForyJsonBuilder withTypeChecker(JsonTypeChecker typeChecker) {
+ this.typeChecker = typeChecker;
+ return this;
+ }
+
public ForyJson build() {
return new ForyJson(
new JsonConfig(
@@ -84,6 +96,7 @@ public final class ForyJsonBuilder {
asyncCompilationEnabled,
propertyDiscoveryEnabled,
maxDepth,
- codecRegistry));
+ codecRegistry,
+ typeChecker));
}
}
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
index a725db01f..7a91e9142 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java
@@ -33,6 +33,8 @@ public final class JsonConfig {
private final boolean propertyDiscoveryEnabled;
private final int maxDepth;
private final CodecRegistry codecRegistry;
+ private final JsonTypeChecker typeChecker;
+ private final JsonTypeCheckContext typeCheckContext;
private final String codecRegistryKey;
private final CodegenKey codegenKey;
private transient int codegenHash;
@@ -43,13 +45,16 @@ public final class JsonConfig {
boolean asyncCompilationEnabled,
boolean propertyDiscoveryEnabled,
int maxDepth,
- CodecRegistry codecRegistry) {
+ CodecRegistry codecRegistry,
+ JsonTypeChecker typeChecker) {
this.writeNullFields = writeNullFields;
this.codegenEnabled = codegenEnabled;
this.asyncCompilationEnabled = asyncCompilationEnabled;
this.propertyDiscoveryEnabled = propertyDiscoveryEnabled;
this.maxDepth = maxDepth;
this.codecRegistry = codecRegistry;
+ this.typeChecker = typeChecker;
+ typeCheckContext = new JsonTypeCheckContext();
codecRegistryKey = codecRegistry.codegenKey();
codegenKey = new CodegenKey(writeNullFields, propertyDiscoveryEnabled,
codecRegistryKey);
}
@@ -78,6 +83,14 @@ public final class JsonConfig {
return codecRegistry;
}
+ public JsonTypeChecker typeChecker() {
+ return typeChecker;
+ }
+
+ public JsonTypeCheckContext typeCheckContext() {
+ return typeCheckContext;
+ }
+
@Override
public boolean equals(Object other) {
if (this == other) {
@@ -92,18 +105,20 @@ public final class JsonConfig {
&& asyncCompilationEnabled == that.asyncCompilationEnabled
&& propertyDiscoveryEnabled == that.propertyDiscoveryEnabled
&& maxDepth == that.maxDepth
+ && typeChecker == that.typeChecker
&& Objects.equals(codecRegistryKey, that.codecRegistryKey);
}
@Override
public int hashCode() {
- return Objects.hash(
- writeNullFields,
- codegenEnabled,
- asyncCompilationEnabled,
- propertyDiscoveryEnabled,
- maxDepth,
- codecRegistryKey);
+ int result = Boolean.hashCode(writeNullFields);
+ result = 31 * result + Boolean.hashCode(codegenEnabled);
+ result = 31 * result + Boolean.hashCode(asyncCompilationEnabled);
+ result = 31 * result + Boolean.hashCode(propertyDiscoveryEnabled);
+ result = 31 * result + maxDepth;
+ result = 31 * result + System.identityHashCode(typeChecker);
+ result = 31 * result + codecRegistryKey.hashCode();
+ return result;
}
private static final AtomicInteger COUNTER = new AtomicInteger(0);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeCheckContext.java
b/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeCheckContext.java
new file mode 100644
index 000000000..07b2e10a8
--- /dev/null
+++
b/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeCheckContext.java
@@ -0,0 +1,25 @@
+/*
+ * 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.fory.json;
+
+/** Context passed to {@link JsonTypeChecker}. */
+public final class JsonTypeCheckContext {
+ JsonTypeCheckContext() {}
+}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeChecker.java
b/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeChecker.java
new file mode 100644
index 000000000..b31f271fe
--- /dev/null
+++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonTypeChecker.java
@@ -0,0 +1,46 @@
+/*
+ * 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.fory.json;
+
+import org.apache.fory.exception.InsecureException;
+
+/**
+ * Checks whether Fory JSON may create or reuse metadata for a Java class name.
+ *
+ * <p>Implementations must be thread-safe. Fory JSON invokes the checker from
shared resolver and
+ * codec setup paths and does not synchronize around user checker state. The
class name is passed as
+ * a string so future class-name materialization paths can run policy before
loading a class.
+ *
+ * <p>Classes served by default built-in exact JSON codecs do not invoke the
configured checker
+ * unless a custom codec is registered for the same class.
+ */
+@FunctionalInterface
+public interface JsonTypeChecker {
+ /**
+ * Returns whether {@code className} is allowed for Fory JSON serialization
or parsing.
+ *
+ * @param className full Java class name
+ * @param context checker context owned by the {@link ForyJson} instance
+ * @return true when the class name is allowed
+ * @throws InsecureException if the implementation rejects the class name by
throwing instead of
+ * returning false
+ */
+ boolean checkType(String className, JsonTypeCheckContext context);
+}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java
index 631141b2d..eac766aae 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java
@@ -435,6 +435,7 @@ public abstract class BaseObjectCodec extends
AbstractJsonCodec {
int modifiers = field.getModifiers();
return !Modifier.isStatic(modifiers)
&& !Modifier.isTransient(modifiers)
+ && field.getType() != Class.class
&& !field.isSynthetic();
}
@@ -447,7 +448,9 @@ public abstract class BaseObjectCodec extends
AbstractJsonCodec {
}
private static String getterPropertyName(Method method) {
- if (method.getParameterCount() != 0 || method.getReturnType() ==
void.class) {
+ if (method.getParameterCount() != 0
+ || method.getReturnType() == void.class
+ || method.getReturnType() == Class.class) {
return null;
}
String name = method.getName();
@@ -466,7 +469,9 @@ public abstract class BaseObjectCodec extends
AbstractJsonCodec {
}
private static String setterPropertyName(Method method) {
- if (method.getParameterCount() != 1 || method.getReturnType() !=
void.class) {
+ if (method.getParameterCount() != 1
+ || method.getReturnType() != void.class
+ || method.getParameterTypes()[0] == Class.class) {
return null;
}
String name = method.getName();
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
index ace05e65c..2e7a9edf6 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java
@@ -74,6 +74,7 @@ public abstract class CollectionCodec extends
AbstractJsonCodec {
TypeRef<?> elementTypeRef = CodecUtils.elementTypeRef(typeRef);
Type elementType = elementTypeRef.getType();
Class<?> elementRawType = CodecUtils.rawType(elementType, Object.class);
+ resolver.checkSecure(elementRawType);
CollectionFactory factory = collectionFactory(rawType, elementRawType);
if (elementRawType == String.class) {
return new StringCollectionCodec(typeRef, factory);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
index 680fc12e0..20846a556 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
@@ -64,6 +64,8 @@ public abstract class MapCodec extends AbstractJsonCodec {
Class<?> keyRawType = CodecUtils.rawType(keyType, Object.class);
Type valueType = keyValueTypeRefs.f1.getType();
Class<?> valueRawType = CodecUtils.rawType(valueType, Object.class);
+ resolver.checkSecure(keyRawType);
+ resolver.checkSecure(valueRawType);
MapFactory factory = mapFactory(rawType, keyRawType);
if (keyRawType == String.class) {
if (valueRawType == String.class) {
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
index e4623c0ba..35a830a6e 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java
@@ -21,8 +21,10 @@ package org.apache.fory.json.resolver;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.fory.json.codec.JsonCodec;
@@ -58,6 +60,14 @@ public final class CodecRegistry {
return new CodecRegistry(copied);
}
+ Set<String> classNames() {
+ Set<String> names = new HashSet<>(codecs.size());
+ for (Class<?> type : codecs.keySet()) {
+ names.add(type.getName());
+ }
+ return names;
+ }
+
public String codegenKey() {
List<Map.Entry<Class<?>, JsonCodec>> entries = new
ArrayList<>(codecs.entrySet());
entries.sort(Comparator.comparing(entry -> entry.getKey().getName()));
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index 40556e962..1ed2874be 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -52,6 +52,7 @@ import java.util.Calendar;
import java.util.Collection;
import java.util.Currency;
import java.util.Date;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Locale;
import java.util.Map;
@@ -59,8 +60,10 @@ import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
+import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
@@ -69,8 +72,11 @@ import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.regex.Pattern;
+import org.apache.fory.exception.InsecureException;
import org.apache.fory.json.ForyJsonException;
import org.apache.fory.json.JsonConfig;
+import org.apache.fory.json.JsonTypeCheckContext;
+import org.apache.fory.json.JsonTypeChecker;
import org.apache.fory.json.codec.ArrayCodec;
import org.apache.fory.json.codec.BaseObjectCodec;
import org.apache.fory.json.codec.CodecUtils;
@@ -86,19 +92,33 @@ import
org.apache.fory.json.codegen.JsonCodegen.GeneratedObjectCodecClasses;
import org.apache.fory.json.codegen.JsonJITContext;
import org.apache.fory.json.meta.JsonFieldKind;
import org.apache.fory.reflect.TypeRef;
+import org.apache.fory.resolver.DisallowedList;
import org.apache.fory.type.BFloat16;
import org.apache.fory.type.Float16;
+import org.apache.fory.type.TypeUtils;
/** Shared JSON codec registry used by all local resolvers for one {@code
ForyJson}. */
public final class JsonSharedRegistry {
+ private static final int TYPE_CHECK_CACHE_LIMIT = 8192;
+
private final CodecRegistry customCodecs;
private final IdentityHashMap<Class<?>, JsonCodec> exactCodecs;
+ private final Set<String> defaultExactCodecNames;
+ private final Set<String> customCodecNames;
+ private final JsonTypeChecker typeChecker;
+ private final JsonTypeCheckContext typeCheckContext;
+ private final ConcurrentHashMap<String, Boolean> typeCheckCache;
+ private final Object typeCheckCacheLock;
private final JsonCodegen codegen;
private final JsonJITContext jitContext;
private final boolean propertyDiscoveryEnabled;
public JsonSharedRegistry(JsonConfig config) {
this.customCodecs = config.codecRegistry().copy();
+ typeChecker = config.typeChecker();
+ typeCheckContext = config.typeCheckContext();
+ typeCheckCache = typeChecker == null ? null : new ConcurrentHashMap<>();
+ typeCheckCacheLock = typeChecker == null ? null : new Object();
this.propertyDiscoveryEnabled = config.propertyDiscoveryEnabled();
exactCodecs = new IdentityHashMap<>();
boolean codegenEnabled = config.codegenEnabled();
@@ -106,6 +126,8 @@ public final class JsonSharedRegistry {
codegen =
codegenEnabled ? new JsonCodegen(config.writeNullFields(),
config.getCodegenHash()) : null;
registerExactCodecs();
+ defaultExactCodecNames = classNames(exactCodecs);
+ customCodecNames = customCodecs.classNames();
}
public JsonCodec createCodec(
@@ -264,6 +286,93 @@ public final class JsonSharedRegistry {
return propertyDiscoveryEnabled;
}
+ void checkSecure(String className) {
+ if (!isSecureName(className)) {
+ throw forbiddenClass(className);
+ }
+ }
+
+ void checkSecure(Class<?> type) {
+ if (!isSecureType(type)) {
+ throw forbiddenClass(type.getName());
+ }
+ }
+
+ private boolean isSecureType(Class<?> type) {
+ if (type.isArray()) {
+ return isSecureType(TypeUtils.getArrayComponent(type));
+ }
+ if (!type.isEnum() && Enum.class.isAssignableFrom(type) && type !=
Enum.class) {
+ Class<?> enclosingClass = type.getEnclosingClass();
+ if (enclosingClass != null && enclosingClass.isEnum()) {
+ return isSecureType(enclosingClass);
+ }
+ }
+ String className = type.getName();
+ DisallowedList.checkNotInDisallowedList(className);
+ return isSecureLeafName(className);
+ }
+
+ private boolean isSecureName(String className) {
+ DisallowedList.checkNotInDisallowedList(className);
+ return isSecureLeafName(className);
+ }
+
+ private boolean isSecureLeafName(String className) {
+ if (defaultExactCodecNames.contains(className) &&
!customCodecNames.contains(className)) {
+ return true;
+ }
+ JsonTypeChecker checker = typeChecker;
+ if (checker == null) {
+ return true;
+ }
+ return checkType(className, checker);
+ }
+
+ private boolean checkType(String className, JsonTypeChecker checker) {
+ ConcurrentHashMap<String, Boolean> cache = typeCheckCache;
+ Boolean cached = cache.get(className);
+ if (cached != null) {
+ return cached.booleanValue();
+ }
+ if (cache.size() >= TYPE_CHECK_CACHE_LIMIT) {
+ return checker.checkType(className, typeCheckContext);
+ }
+ // Keep cacheable cold-path misses under one lock so concurrent duplicate
names publish
+ // exactly one checker decision without allocating per-name futures or
holders.
+ synchronized (typeCheckCacheLock) {
+ cached = cache.get(className);
+ if (cached != null) {
+ return cached.booleanValue();
+ }
+ if (cache.size() >= TYPE_CHECK_CACHE_LIMIT) {
+ return checker.checkType(className, typeCheckContext);
+ }
+ boolean allowed;
+ try {
+ allowed = checker.checkType(className, typeCheckContext);
+ } catch (InsecureException e) {
+ cache.put(className, false);
+ throw e;
+ }
+ cache.put(className, allowed);
+ return allowed;
+ }
+ }
+
+ private static InsecureException forbiddenClass(String className) {
+ return new InsecureException(
+ String.format("Class %s is forbidden for Fory JSON serialization.",
className));
+ }
+
+ private static Set<String> classNames(Map<Class<?>, JsonCodec> codecs) {
+ Set<String> names = new HashSet<>(codecs.size());
+ for (Class<?> type : codecs.keySet()) {
+ names.add(type.getName());
+ }
+ return names;
+ }
+
private void registerExactCodecs() {
exactCodecs.put(Object.class, ScalarCodecs.NaturalCodec.INSTANCE);
exactCodecs.put(void.class, ScalarCodecs.VoidCodec.INSTANCE);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
index 3b363fffc..186abc6d1 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java
@@ -117,11 +117,16 @@ public final class JsonTypeResolver {
}
}
+ public void checkSecure(Class<?> type) {
+ sharedRegistry.checkSecure(type);
+ }
+
private BaseObjectCodec buildObjectCodec(Class<?> type) {
BaseObjectCodec cached = objectCodecs.get(type);
if (cached != null) {
return cached;
}
+ sharedRegistry.checkSecure(type);
ObjectCodec codec = BaseObjectCodec.build(type,
sharedRegistry.propertyDiscoveryEnabled());
// Codegen may ask for nested object metadata that points back to this
type.
// Publishing before compiling keeps recursive ownership in this resolver
cache.
@@ -168,6 +173,7 @@ public final class JsonTypeResolver {
}
private JsonTypeInfo buildTypeInfo(Class<?> rawType, Type declaredType) {
+ sharedRegistry.checkSecure(rawType);
TypeRef<?> typeRef = typeRef(declaredType, rawType);
JsonCodec codec = sharedRegistry.createCodec(rawType, typeRef, this);
if (codec == null) {
@@ -184,6 +190,7 @@ public final class JsonTypeResolver {
if (cached != null) {
return cached;
}
+ sharedRegistry.checkSecure(rawType);
TypeRef<?> typeRef = TypeRef.of(rawType);
JsonCodec codec =
rawType == Object.class
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
index ffb50b85f..eca53bb5b 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java
@@ -858,12 +858,12 @@ public class JsonScalarTest extends ForyJsonTestModels {
}
@Test
- public void rejectClassFields() {
+ public void skipClassFields() {
ForyJson json = newJson();
- assertThrows(ForyJsonException.class, () -> json.toJson(new
ClassFieldHolder()));
- assertThrows(
- ForyJsonException.class,
- () -> json.fromJson("{\"type\":\"java.lang.String\"}",
ClassFieldHolder.class));
+ assertEquals(json.toJson(new ClassFieldHolder()), "{}");
+ ClassFieldHolder holder =
+ json.fromJson("{\"type\":\"java.lang.Integer\"}",
ClassFieldHolder.class);
+ assertEquals(holder.type, String.class);
}
@Test
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java
new file mode 100644
index 000000000..7cf2df793
--- /dev/null
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java
@@ -0,0 +1,317 @@
+/*
+ * 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.fory.json;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertThrows;
+
+import java.beans.Expression;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.fory.annotation.Expose;
+import org.apache.fory.exception.InsecureException;
+import org.apache.fory.json.codec.JsonCodec;
+import org.apache.fory.json.data.Kind;
+import org.apache.fory.json.reader.JsonReader;
+import org.apache.fory.json.resolver.CodecRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonTypeInfo;
+import org.apache.fory.json.resolver.JsonTypeResolver;
+import org.apache.fory.json.writer.JsonWriter;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.reflect.TypeRef;
+import org.testng.annotations.Factory;
+import org.testng.annotations.Test;
+
+public class JsonTypeCheckerTest extends ForyJsonTestModels {
+ private static final JsonCodec NULL_CODEC = new NullCodec();
+
+ @Factory(dataProvider = "codegen")
+ public JsonTypeCheckerTest(boolean codegen) {
+ super(codegen);
+ }
+
+ @Test
+ public void defaultRejectsDisallowedWrite() {
+ ForyJson json = newJson();
+ assertThrows(
+ InsecureException.class,
+ () -> json.toJson(new Expression(System.class, "exit", new Object[]
{0})));
+ }
+
+ @Test
+ public void defaultRejectsDisallowedRead() {
+ ForyJson json = newJson();
+ assertThrows(InsecureException.class, () -> json.fromJson("{}",
Expression.class));
+ }
+
+ @Test
+ public void checkerRejectsRootWrite() {
+ ForyJson json = rejectingJson(CheckedBean.class);
+ assertThrows(InsecureException.class, () -> json.toJson(new
CheckedBean()));
+ }
+
+ @Test
+ public void checkerRejectsRootRead() {
+ ForyJson json = rejectingJson(CheckedBean.class);
+ assertThrows(InsecureException.class, () -> json.fromJson("{}",
CheckedBean.class));
+ }
+
+ @Test
+ public void defaultExactSkipsChecker() {
+ AtomicInteger calls = new AtomicInteger();
+ ForyJson json =
+ newJsonBuilder()
+ .withTypeChecker(
+ (className, context) -> {
+ calls.incrementAndGet();
+ return false;
+ })
+ .build();
+ assertEquals(json.toJson("value"), "\"value\"");
+ assertEquals(calls.get(), 0);
+ }
+
+ @Test
+ public void customExactUsesChecker() {
+ ForyJson json =
+ newJsonBuilder()
+ .registerCodec(String.class, NULL_CODEC)
+ .withTypeChecker((className, context) ->
!className.equals(String.class.getName()))
+ .build();
+ assertThrows(InsecureException.class, () -> json.toJson("value"));
+ }
+
+ @Test
+ public void customPrimitiveUsesChecker() {
+ ForyJson json =
+ newJsonBuilder()
+ .registerCodec(int.class, NULL_CODEC)
+ .withTypeChecker((className, context) ->
!className.equals(int.class.getName()))
+ .build();
+ assertThrows(InsecureException.class, () -> json.fromJson("1", int.class));
+ }
+
+ @Test
+ public void contextHasNoClass() {
+ JsonTypeCheckContext[] seen = new JsonTypeCheckContext[1];
+ ForyJson json =
+ newJsonBuilder()
+ .withTypeChecker(
+ (className, context) -> {
+ seen[0] = context;
+ return true;
+ })
+ .build();
+ json.toJson(new CheckedBean());
+ assertNotNull(seen[0]);
+ for (Method method : JsonTypeCheckContext.class.getDeclaredMethods()) {
+ assertNotSame(method.getReturnType(), Class.class);
+ }
+ }
+
+ @Test
+ public void duplicateChecksCached() {
+ AtomicInteger calls = new AtomicInteger();
+ ForyJson json =
+ newJsonBuilder()
+ .withTypeChecker(
+ (className, context) -> {
+ calls.incrementAndGet();
+ return true;
+ })
+ .build();
+ json.toJson(new CheckedBean());
+ json.toJson(new CheckedBean());
+ assertEquals(calls.get(), 1);
+ }
+
+ @Test
+ public void deniedChecksCached() {
+ AtomicInteger calls = new AtomicInteger();
+ ForyJson json =
+ newJsonBuilder()
+ .withTypeChecker(
+ (className, context) -> {
+ calls.incrementAndGet();
+ return !className.equals(CheckedBean.class.getName());
+ })
+ .build();
+ assertThrows(InsecureException.class, () -> json.toJson(new
CheckedBean()));
+ assertThrows(InsecureException.class, () -> json.toJson(new
CheckedBean()));
+ assertEquals(calls.get(), 1);
+ }
+
+ @Test
+ public void cacheCapChecksOverflow() throws Exception {
+ AtomicInteger calls = new AtomicInteger();
+ JsonTypeChecker checker =
+ (className, context) -> {
+ calls.incrementAndGet();
+ return true;
+ };
+ JsonSharedRegistry registry =
+ new JsonSharedRegistry(
+ new JsonConfig(
+ false,
+ false,
+ false,
+ true,
+ ForyJson.DEFAULT_MAX_DEPTH,
+ new CodecRegistry(),
+ checker));
+ Method checkSecure =
JsonSharedRegistry.class.getDeclaredMethod("checkSecure", String.class);
+ checkSecure.setAccessible(true);
+ for (int i = 0; i < 8192; i++) {
+ invokeCheck(checkSecure, registry, "example.Type" + i);
+ }
+ assertEquals(calls.get(), 8192);
+ invokeCheck(checkSecure, registry, "example.Overflow");
+ invokeCheck(checkSecure, registry, "example.Overflow");
+ assertEquals(calls.get(), 8194);
+ invokeCheck(checkSecure, registry, "example.Type0");
+ assertEquals(calls.get(), 8194);
+ }
+
+ @Test
+ public void nestedFieldRejected() {
+ ForyJson json = rejectingJson(RejectedValue.class);
+ assertThrows(
+ InsecureException.class, () -> json.fromJson("{\"value\":{}}",
RejectedHolder.class));
+ }
+
+ @Test
+ public void collectionScalarChecked() {
+ ForyJson json =
+ newJsonBuilder()
+ .registerCodec(Integer.class, NULL_CODEC)
+ .withTypeChecker((className, context) ->
!className.equals(Integer.class.getName()))
+ .build();
+ assertThrows(
+ InsecureException.class, () -> json.fromJson("[1]", new
TypeRef<List<Integer>>() {}));
+ }
+
+ @Test
+ public void mapScalarChecked() {
+ ForyJson json =
+ newJsonBuilder()
+ .registerCodec(Integer.class, NULL_CODEC)
+ .withTypeChecker((className, context) ->
!className.equals(Integer.class.getName()))
+ .build();
+ assertThrows(
+ InsecureException.class,
+ () -> json.fromJson("{\"one\":1}", new TypeRef<Map<String, Integer>>()
{}));
+ }
+
+ @Test
+ public void enumMapKeyChecked() {
+ ForyJson json = rejectingJson(Kind.class);
+ assertThrows(
+ InsecureException.class,
+ () -> json.fromJson("{\"FAST\":\"ok\"}", new TypeRef<Map<Kind,
String>>() {}));
+ }
+
+ @Test
+ public void classMembersSkipped() {
+ ForyJson json = rejectingJson(Class.class);
+ ClassMembers value = json.fromJson("{\"type\":\"java.lang.String\"}",
ClassMembers.class);
+ assertEquals(json.toJson(value), "{}");
+ }
+
+ @Test
+ public void classExposeSkipped() {
+ ForyJson json = rejectingJson(Class.class);
+ assertEquals(json.toJson(new ExposedClassMember()), "{\"id\":7}");
+ }
+
+ private ForyJson rejectingJson(Class<?> rejectedType) {
+ String rejectedName = rejectedType.getName();
+ return newJsonBuilder()
+ .withTypeChecker((className, context) ->
!className.equals(rejectedName))
+ .build();
+ }
+
+ private static void invokeCheck(Method checkSecure, JsonSharedRegistry
registry, String name)
+ throws Exception {
+ try {
+ checkSecure.invoke(registry, name);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ }
+ throw e;
+ }
+ }
+
+ public static final class CheckedBean {}
+
+ public static final class RejectedValue {}
+
+ public static final class RejectedHolder {
+ public RejectedValue value;
+ }
+
+ public static final class ClassMembers {
+ public Class<?> type = String.class;
+
+ public Class<?> getModelClass() {
+ return Integer.class;
+ }
+
+ public void setModelClass(Class<?> modelClass) {
+ throw new AssertionError("Class setter should be ignored");
+ }
+ }
+
+ public static final class ExposedClassMember {
+ @Expose public Class<?> type = String.class;
+ public int id = 7;
+ }
+
+ private static final class NullCodec implements JsonCodec {
+ @Override
+ public void write(JsonWriter writer, Object value, JsonTypeResolver
resolver) {
+ writer.writeNull();
+ }
+
+ @Override
+ public void writeString(StringJsonWriter writer, Object value,
JsonTypeResolver resolver) {
+ writer.writeNull();
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, Object value,
JsonTypeResolver resolver) {
+ writer.writeNull();
+ }
+
+ @Override
+ public Object read(JsonReader reader, JsonTypeInfo typeInfo,
JsonTypeResolver resolver) {
+ reader.skipValue();
+ return null;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]