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]


Reply via email to