diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderImplClass.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderImplClass.java index d46e575159..2a75322b2f 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderImplClass.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderImplClass.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONException; import com.alibaba.fastjson2.JSONReader; import com.alibaba.fastjson2.util.Fnv; +import com.alibaba.fastjson2.util.JDKUtils; import com.alibaba.fastjson2.util.TypeUtils; import java.lang.reflect.Type; @@ -47,6 +48,19 @@ public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName } String className = jsonReader.getString(); + + // Defence-in-depth: refuse to load any class whose name matches the FQCN deny table before + // invoking TypeUtils.loadClass. Without this check, an attacker controlling JSON input + // could send {"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"} + // and rely on the outer checkAutoType to silently permit the resolution because the inner + // Class.forName runs in a privileged context. This makes the policy explicit and matches + // the same safety check applied to the fastjson 1.x MiscCodec branch. + for (int i = 0; i < JDKUtils.AUTO_TYPE_DENY_FQCN.length; i++) { + if (className.equals(JDKUtils.AUTO_TYPE_DENY_FQCN[i])) { + throw new JSONException(jsonReader.info("autoType is not support : " + className)); + } + } + boolean classForName = ((context.getFeatures() | features) & JSONReader.Feature.SupportClassForName.mask) != 0; if (!classForName) { String msg = jsonReader.info("not support ClassForName : " + className + ", you can config 'JSONReader.Feature.SupportClassForName'"); @@ -63,6 +77,13 @@ public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName if (resolvedClass == null) { throw new JSONException(jsonReader.info("class not found " + className)); } + // Defence-in-depth: even after the outer checkAutoType gate has run, re-check the resolved + // class against the FQCN deny table. This closes the residual surface where + // SupportClassForName is opted in and the outer checkAutoType would otherwise have to + // assume the resolved class is safe. + if (JDKUtils.isAutoTypeDenyClass(resolvedClass)) { + throw new JSONException(jsonReader.info("autoType is not support : " + className)); + } return resolvedClass; } } diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java index 5fd1a81b04..372c3b4fa0 100644 --- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java +++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderProvider.java @@ -214,6 +214,15 @@ public ObjectReaderCachePair(long hashCode, ObjectReader reader) { */ private volatile Set acceptNameSet = Collections.emptySet(); + /** + * Programmatic deny list registered via {@link #addAutoTypeDeny(String)}. Independent from the + * hardcoded FQCN list in {@link JDKUtils#AUTO_TYPE_DENY_FQCN}; both lists are consulted on every + * {@link #checkAutoType} call. Names are normalized the same way as {@link #acceptNameSet} so a + * {@code $} ↔ {@code .} rewrite cannot smuggle a denied entry past the rolling-hash scan. + */ + private volatile long[] denyHashCodes = new long[0]; + private volatile Set denyNameSet = Collections.emptySet(); + private AutoTypeBeforeHandler autoTypeBeforeHandler = DEFAULT_AUTO_TYPE_BEFORE_HANDLER; private Consumer autoTypeHandler = DEFAULT_AUTO_TYPE_HANDLER; PropertyNamingStrategy namingStrategy; @@ -240,6 +249,22 @@ public ObjectReaderCachePair(long hashCode, ObjectReader reader) { acceptNameSet = Collections.unmodifiableSet(names); acceptHashCodes = hashCodes; + // Seed the programmatic deny list from the JVM-wide fastjson2.parser.deny system property + // so that setting it at JVM start actually works in fastjson2 (this was previously only + // honoured by fastjson 1.x's ParserConfig; the 2.x Provider silently ignored it). + String denyProp = System.getProperty("fastjson2.parser.deny"); + if (denyProp == null || denyProp.isEmpty()) { + denyProp = JSONFactory.Conf.getProperty("fastjson2.parser.deny"); + } + if (denyProp != null && !denyProp.isEmpty()) { + for (String item : denyProp.split(",")) { + String trimmed = item.trim(); + if (!trimmed.isEmpty()) { + addAutoTypeDeny(trimmed); + } + } + } + hashCache.put(ObjectArrayReader.TYPE_HASH_CODE, ObjectArrayReader.INSTANCE); final long STRING_CLASS_NAME_HASH = -4834614249632438472L; // Fnv.hashCode64(String.class.getName()); hashCache.put(STRING_CLASS_NAME_HASH, ObjectReaderImplString.INSTANCE); @@ -299,8 +324,40 @@ public synchronized void addAutoTypeAccept(String name) { } } - @Deprecated - public void addAutoTypeDeny(String name) { + /** + * Adds a type name to the programmatic deny list. Types on this list are rejected by + * {@link #checkAutoType} regardless of {@code SupportAutoType} or any explicit accept entry. + * Previously this method was a no-op {@code @Deprecated} stub; this restores parity with + * {@code com.alibaba.fastjson.parser.ParserConfig#addDeny} so that downstream apps that + * migrate from fastjson 1.x and rely on the legacy deny API still get the protection they + * expect. + * + * @param name the type name to add (matched after {@code $} ↔ {@code .} normalization, + * exactly the same way {@link #addAutoTypeAccept} treats names) + */ + public synchronized void addAutoTypeDeny(String name) { + if (name == null || name.isEmpty()) { + return; + } + String denyName = normalizeAcceptName(name); + + // publish the name before the hash, so that a reader seeing the new hash array is + // guaranteed to see the name it verifies against rather than transiently rejecting + if (!this.denyNameSet.contains(denyName)) { + Set names = new HashSet<>(this.denyNameSet); + names.add(denyName); + this.denyNameSet = Collections.unmodifiableSet(names); + } + + long hash = Fnv.hashCode64(denyName); + long[] current = this.denyHashCodes; + if (Arrays.binarySearch(current, hash) < 0) { + long[] hashCodes = new long[current.length + 1]; + hashCodes[hashCodes.length - 1] = hash; + System.arraycopy(current, 0, hashCodes, 0, current.length); + Arrays.sort(hashCodes); + this.denyHashCodes = hashCodes; + } } /** @@ -858,6 +915,16 @@ public Class checkAutoType(String typeName, Class expectClass, long featur return expectClass; } + // Programmatic deny list check. Run BEFORE any allow-list rolling-hash scan or loadClass + // call so a denied type never triggers a Class.forName on attacker input. The name is + // normalized the same way as acceptNameSet so $ ↔ . rewrites don't smuggle past. + String normalizedDenyName = normalizeAcceptName(typeName); + long denyHash = Fnv.hashCode64(normalizedDenyName); + if (Arrays.binarySearch(denyHashCodes, denyHash) >= 0 + && denyNameSet.contains(normalizedDenyName)) { + throw new JSONException("autoType is not support. " + typeName); + } + boolean autoTypeSupport = (features & JSONReader.Feature.SupportAutoType.mask) != 0; Class clazz; @@ -1535,9 +1602,8 @@ public PropertyNamingStrategy getNamingStrategy() { * Sets the property naming strategy used by this provider. * * @param namingStrategy the property naming strategy to set - * @since 2.0.52 */ public void setNamingStrategy(PropertyNamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java index 9a92435fcc..cebe954ecb 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java @@ -68,6 +68,118 @@ public class JDKUtils { static volatile Throwable reflectErrorLast; static final AtomicInteger reflectErrorCount = new AtomicInteger(); + /** + * Hardcoded FQCN-based deny list for {@link #isAutoTypeDenyClass(Class)}. These classes are + * inherently unsafe when reachable via {@code @type} because their static initializers / + * setters / class-loading behaviour can lead to RCE, SSRF, or LFD even when no further + * gadget-chain setter is involved. Each {@code Class.forName} probe is wrapped in try/catch so + * an unloaded or absent module degrades to a silent skip rather than failing the whole class + * init of {@code JDKUtils}. + */ + static final String[] AUTO_TYPE_DENY_FQCN = new String[] { + "java.lang.Runtime", + "java.lang.Process", + "java.lang.ProcessBuilder", + "java.lang.System", + "java.lang.Thread", + "java.lang.ClassLoader", + "java.lang.Shutdown", + "java.lang.Class", + "java.io.File", + "java.io.FileInputStream", + "java.io.FileOutputStream", + "java.io.ObjectInputStream", + "java.io.ObjectOutputStream", + "java.io.RandomAccessFile", + "java.net.URL", + "java.net.URI", + "java.net.URLClassLoader", + "java.net.InetAddress", + "java.net.InetSocketAddress", + "java.net.Socket", + "java.net.ServerSocket", + "java.net.DatagramSocket", + "java.nio.channels.SocketChannel", + "java.nio.channels.ServerSocketChannel", + "java.rmi.server.UnicastRemoteObject", + "java.rmi.activation.Activator", + "java.beans.XMLDecoder", + "javax.naming.InitialContext", + "javax.script.ScriptEngineManager", + "javax.management.remote.rmi.RMIConnector", + "javax.management.remote.JMXServiceURL", + "javax.imageio.ImageIO", + "javax.activation.MimeType", + "javax.sound.sampled.AudioSystem", + "javax.sound.midi.MidiSystem", + "sun.print.PrintServiceLookup", + "com.sun.rowset.JdbcRowSetImpl", + "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", + "com.sun.org.apache.xpath.internal.jaxp.XPathFactory", + "com.sun.org.apache.bcel.internal.util.ClassLoader", + "com.sun.org.apache.jndi.ldap.LdapCtx", + "com.sun.net.httpserver.HttpServer", + "org.apache.xbean.propertyeditor.JndiConverter", + "org.apache.catalina.startup.Tomcat", + "org.apache.commons.collections.Transformer", + "org.apache.commons.collections.functors.InvokerTransformer", + "org.apache.commons.collections.functors.ChainedTransformer", + "org.apache.commons.collections4.functors.InvokerTransformer", + "org.apache.commons.beanutils.BeanComparator", + "org.apache.commons.io.FileUtils", + "org.apache.commons.io.IOUtils", + "org.apache.commons.lang.SerializationUtils", + "org.springframework.beans.factory.config.PropertyPathFactoryBean", + "org.springframework.context.support.ClassPathXmlApplicationContext", + "org.springframework.context.support.FileSystemXmlApplicationContext", + "org.springframework.aop.aspectj.AspectJExpressionPointcut", + "org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator", + "org.springframework.jndi.JndiTemplate", + "org.springframework.jndi.JndiObjectTargetSource", + "org.springframework.transaction.jta.JtaTransactionManager", + "org.springframework.web.client.RestTemplate", + "org.hibernate.engine.spi.TypedValue", + "org.hibernate.jpa.HibernatePersistenceProvider", + "ch.qos.logback.core.db.JNDIConnectionSource", + "com.mchange.v2.c3p0.JndiRefForwardingDataSource", + "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", + "freemarker.template.utility.Execute", + "freemarker.cache.TemplateLoader", + "ognl.OgnlContext", + "javax.faces.context.FacesContext", + "javax.faces.context.ExternalContext", + "javax.faces.context.ResponseStream", + "javassist.ClassPool", + "javassist.CtClass", + "bsh.Interpreter", + "groovy.lang.GroovyShell", + "org.python.core.PyObject", + "org.codehaus.groovy.runtime.ConvertedClosure", + "org.codehaus.groovy.runtime.MethodClosure", + "kafka.utils.VerifiableProperties" + }; + + /** + * Lazily-loaded Class objects corresponding to {@link #AUTO_TYPE_DENY_FQCN}. A null entry means + * the class was either absent (e.g. optional module not on classpath) or failed to load during + * the static init sweep; the FQCN string itself is the authoritative gate so that a runtime + * {@code Class.forName(name)} on attacker input can be cross-referenced even when this array + * does not have the class loaded. + */ + static final Class[] AUTO_TYPE_DENY_CLASSES; + + static { + Class[] deny = new Class[AUTO_TYPE_DENY_FQCN.length]; + for (int i = 0; i < AUTO_TYPE_DENY_FQCN.length; i++) { + try { + deny[i] = Class.forName(AUTO_TYPE_DENY_FQCN[i]); + } catch (Throwable ignored) { + deny[i] = null; + } + } + AUTO_TYPE_DENY_CLASSES = deny; + } + static { Unsafe unsafe; try { @@ -456,16 +568,40 @@ public static boolean isSQLDataSourceOrRowSet(Class type) { } /** - * Tests whether a class is a well known deserialization gadget entry point, namely a - * {@link ClassLoader} subclass or a JDK SQL {@code DataSource}/{@code RowSet} implementation. - * Such types must not be resolved by matching an autoType whitelist prefix; only an accept - * entry naming the type in full is treated as an explicit opt-in. + * Tests whether a class is unsafe to expose via the autoType mechanism. Three categories are + * blocked: + *
    + *
  1. Subclasses of {@link ClassLoader} — they can load arbitrary code.
  2. + *
  3. Subclasses of {@code javax.sql.DataSource} / {@code javax.sql.RowSet} — classic JNDI + * gadget sinks.
  4. + *
  5. The hardcoded FQCN list in {@link #AUTO_TYPE_DENY_FQCN} — classes whose presence on the + * classpath or whose static initializer / constructor / setter chain is itself enough to + * reach RCE / SSRF / LFD (e.g. {@code java.lang.Runtime}, {@code TemplatesImpl}, + * {@code InvokerTransformer}, {@code URLClassLoader}). The match is done by exact + * FQCN — subclasses of these types are NOT auto-banned so that legitimate app classes + * with the same supertype are not collateral damage.
  6. + *
+ * Subclasses of ClassLoader / DataSource / RowSet are blocked because the existing allow-list + * semantics in {@link com.alibaba.fastjson2.reader.ObjectReaderProvider#checkAutoType} treat a + * single explicit accept as opening the entire type hierarchy. * * @param type the class to test * @return true if the class must not be resolved through a whitelist prefix match */ public static boolean isAutoTypeDenyClass(Class type) { - return ClassLoader.class.isAssignableFrom(type) || isSQLDataSourceOrRowSet(type); + if (ClassLoader.class.isAssignableFrom(type) || isSQLDataSourceOrRowSet(type)) { + return true; + } + if (type == null) { + return false; + } + String name = type.getName(); + for (int i = 0; i < AUTO_TYPE_DENY_FQCN.length; i++) { + if (name.equals(AUTO_TYPE_DENY_FQCN[i])) { + return true; + } + } + return false; } public static void setReflectErrorLast(Throwable error) { diff --git a/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderImplClassTest.java b/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderImplClassTest.java new file mode 100644 index 0000000000..3dd748b3e8 --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderImplClassTest.java @@ -0,0 +1,47 @@ +package com.alibaba.fastjson2.reader; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.util.JDKUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ObjectReaderImplClassTest { + /** + * Verifies PR-2: ObjectReaderImplClass refuses to resolve a class whose FQCN is on the + * {@link JDKUtils#AUTO_TYPE_DENY_FQCN} deny list, even when both SupportAutoType and + * SupportClassForName are opted into. Without the defence-in-depth added in PR-2, this test + * would successfully call Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl") + * and load the TemplatesImpl class. + */ + @Test + public void testRefusesDangerousVal() { + JSONReader.Feature[] features = new JSONReader.Feature[]{ + JSONReader.Feature.SupportAutoType, + JSONReader.Feature.SupportClassForName + }; + String payload = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}"; + assertThrows( + JSONException.class, + () -> JSON.parseObject(payload, Class.class, features) + ); + } + + /** + * Verifies PR-2 also rejects java.lang.Runtime (a 1.x-era gadget) via the same path. + */ + @Test + public void testRefusesRuntimeVal() { + JSONReader.Feature[] features = new JSONReader.Feature[]{ + JSONReader.Feature.SupportAutoType, + JSONReader.Feature.SupportClassForName + }; + String payload = "{\"@type\":\"java.lang.Class\",\"val\":\"java.lang.Runtime\"}"; + assertThrows( + JSONException.class, + () -> JSON.parseObject(payload, Class.class, features) + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderProviderTest.java b/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderProviderTest.java new file mode 100644 index 0000000000..4aa0340a32 --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/reader/ObjectReaderProviderTest.java @@ -0,0 +1,80 @@ +package com.alibaba.fastjson2.reader; + +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONReader; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ObjectReaderProviderTest { + private String savedDenyProp; + + @BeforeEach + public void saveSystemProperty() { + savedDenyProp = System.getProperty("fastjson2.parser.deny"); + System.clearProperty("fastjson2.parser.deny"); + } + + @AfterEach + public void restoreSystemProperty() { + if (savedDenyProp == null) { + System.clearProperty("fastjson2.parser.deny"); + } else { + System.setProperty("fastjson2.parser.deny", savedDenyProp); + } + } + + /** + * Verifies PR-3: ObjectReaderProvider.addAutoTypeDeny(String) now actually adds the type + * to the deny table (it was previously a no-op @Deprecated stub in fastjson 2.x). After + * adding, checkAutoType must throw JSONException for that name. + */ + @Test + public void testAddAutoTypeDenyEnforced() { + ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider(); + provider.addAutoTypeDeny("com.acme.Gadget"); + long features = JSONReader.Feature.SupportAutoType.mask; + assertThrows( + JSONException.class, + () -> provider.checkAutoType("com.acme.Gadget", null, features) + ); + } + + /** + * Verifies PR-3: the $ → . rewrite is normalised before the deny check, so a class with + * an inner-type FQCN ("com.acme.Gadget$Inner") denied by name also blocks the dot-form + * ("com.acme.Gadget.Inner") from sneaking through the rolling-hash allow-list. + */ + @Test + public void testDenyNormalisationDollarToDot() { + ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider(); + provider.addAutoTypeDeny("com.acme.Gadget$Inner"); + long features = JSONReader.Feature.SupportAutoType.mask; + assertThrows( + JSONException.class, + () -> provider.checkAutoType("com.acme.Gadget.Inner", null, features) + ); + } + + /** + * Verifies PR-3: setting the fastjson2.parser.deny system property at JVM start actually + * seeds the deny list (previously the property was honoured only by fastjson 1.x's + * ParserConfig; the 2.x Provider silently ignored it). + */ + @Test + public void testSystemPropertyDenySeed() { + System.setProperty("fastjson2.parser.deny", "com.acme.X,com.acme.Y"); + ObjectReaderProvider provider = new ObjectReaderProvider(); + long features = JSONReader.Feature.SupportAutoType.mask; + assertThrows( + JSONException.class, + () -> provider.checkAutoType("com.acme.X", null, features) + ); + assertThrows( + JSONException.class, + () -> provider.checkAutoType("com.acme.Y", null, features) + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/alibaba/fastjson2/util/JDKUtilsDenyListTest.java b/core/src/test/java/com/alibaba/fastjson2/util/JDKUtilsDenyListTest.java new file mode 100644 index 0000000000..fc826f483b --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/util/JDKUtilsDenyListTest.java @@ -0,0 +1,39 @@ +package com.alibaba.fastjson2.util; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static com.alibaba.fastjson2.util.JDKUtils.AUTO_TYPE_DENY_FQCN; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("util") +public class JDKUtilsDenyListTest { + /** + * Verifies PR-1: every FQCN in JDKUtils.AUTO_TYPE_DENY_FQCN is recognised by + * {@link JDKUtils#isAutoTypeDenyClass(Class)} as a deny entry. Sibling classes in the same + * package must NOT be blocked (the match is FQCN-exact, not isAssignableFrom, so legitimate + * app classes inheriting from a safe type are not collateral damage). + */ + @Test + public void testIsAutoTypeDenyClass_ExactFQCN() throws Exception { + for (String fqcn : AUTO_TYPE_DENY_FQCN) { + Class clazz; + try { + clazz = Class.forName(fqcn); + } catch (ClassNotFoundException e) { + // Optional module not on classpath; the FQCN string itself is the authoritative + // gate, so a missing class does not fail this assertion. + continue; + } + assertTrue(JDKUtils.isAutoTypeDenyClass(clazz), + "isAutoTypeDenyClass must return true for " + fqcn); + } + + // A non-deny sibling must NOT be matched: Object is in java.lang but is not a gadget. + assertFalse(JDKUtils.isAutoTypeDenyClass(Object.class), + "java.lang.Object must NOT be on the deny list"); + assertFalse(JDKUtils.isAutoTypeDenyClass(String.class), + "java.lang.String must NOT be on the deny list"); + } +} \ No newline at end of file