-
Notifications
You must be signed in to change notification settings - Fork 612
security: expand autoType deny list + implement addAutoTypeDeny (5 commits, closes 5 VULNs, tests included) #7846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1c21cbd
6ad74fa
11088ae
c00e3fd
e057e26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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++) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-1: No jar is produced, so Witness: Please do not fix this by adding // JDKUtils
private static final Set<String> AUTO_TYPE_DENY_FQCN_SET =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AUTO_TYPE_DENY_FQCN)));
public static boolean isAutoTypeDenyName(String name) {
return name != null && AUTO_TYPE_DENY_FQCN_SET.contains(name);
}// ObjectReaderImplClass.readObject, replacing the open-coded loop
if (JDKUtils.isAutoTypeDenyName(className)) {
throw new JSONException(jsonReader.info("autoType is not support : " + className));
}This also removes the duplicated 80-iteration linear scan and brings Constraint: Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,6 +214,15 @@ public ObjectReaderCachePair(long hashCode, ObjectReader reader) { | |
| */ | ||
| private volatile Set<String> 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<String> denyNameSet = Collections.emptySet(); | ||
|
|
||
| private AutoTypeBeforeHandler autoTypeBeforeHandler = DEFAULT_AUTO_TYPE_BEFORE_HANDLER; | ||
| private Consumer<Class> 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"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-15: this instance initializer re-reads
Witness: Use the constant for both reads and then pick one owner: either delete the now-doubly-dead Constraint: Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| if (denyProp == null || denyProp.isEmpty()) { | ||
| denyProp = JSONFactory.Conf.getProperty("fastjson2.parser.deny"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-23: this seeding reads only the v2 spelling
Witness: The migrating operator is real, not hypothetical: Constraint: Acceptance: a new case that sets only — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| } | ||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-7: [certifies-falsely] [new-surface] a deny entry is stored and matched as ONE exact The javadoc this diff adds promises "restores parity with
Witness: Decide what a deny key is and enforce it once, at registration and at lookup alike: (a) trim here — Three constraints, all measured. Acceptance: four assertions, each red today — — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
|
|
||
| // publish the name before the hash, so that a reader seeing the new hash array is | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-16: First, the sorted Witness: Delete Constraint: the accept-side ordering at Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| // guaranteed to see the name it verifies against rather than transiently rejecting | ||
| if (!this.denyNameSet.contains(denyName)) { | ||
| Set<String> 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-6: [certifies-falsely] [new-surface] the programmatic deny list is read at exactly ONE place in all of This is a class-level finding rather than four separate ones, because the surface is not closable entrance by entrance — five successive audit passes each found another route. The root is that the policy is one inline block in one method instead of one owner consulted at every route that turns a name into a The four measured entrances:
Witness: Close it structurally rather than route by route: add Two constraints that were measured, so please do not skip them. First, do not reorder Acceptance: three tests, each red today — (a) — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| // 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-18: this block runs unconditionally on every It sits above Witness: Read the volatile once into a local and short-circuit before touching the name: long[] denies = this.denyHashCodes; // one volatile read
if (denies.length != 0) {
String normalizedDenyName = normalizeAcceptName(typeName);
if (Arrays.binarySearch(denies, Fnv.hashCode64(normalizedDenyName)) >= 0
&& denyNameSet.contains(normalizedDenyName)) {
throw new JSONException("autoType is not support. " + typeName);
}
}Keeping the check ahead of the accept scan and of Constraint: Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-22: the deny block sits above this That contradicts the standing policy this same method documents at Witness (same probe compiled once, run unchanged on both arms, deny registered, autoType OFF): Compute Two corrections so this is reproducible: plain Constraint: Acceptance: a new case asserting — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| 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; | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
1607
to
+1609
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-3: four files in this PR do not end with a newline, and this hunk removed the trailing LF
Witness: Append a single LF to the end of all four files. Constraint: the separator must be LF, not CRLF ( Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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[] { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-21: the security policy table is published as a mutable Two routes, both real. (1) On a classpath (non-JPMS) build any jar may declare a class in Witness: Keep the array This stays a Suggestion rather than higher because the writer must already be executing code in the JVM (a split-package jar, or reflection via the Constraint: Acceptance: an assertion that the published table is immutable (e.g. — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| "java.lang.Runtime", | ||||||
| "java.lang.Process", | ||||||
| "java.lang.ProcessBuilder", | ||||||
| "java.lang.System", | ||||||
| "java.lang.Thread", | ||||||
| "java.lang.ClassLoader", | ||||||
| "java.lang.Shutdown", | ||||||
| "java.lang.Class", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-5: the table contains ordinary supported value types —
Witness:
Constraint: Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| "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", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R1-8: [certifies-falsely] [new-surface] this is a class-level finding about the table's shape, not about any one entry. The The javadoc below states the gap outright — "The match is done by exact FQCN — subclasses of these types are NOT auto-banned" — and that is a real argument against Witness:
Close the class, not the corner: (1) match by hierarchy where the supertype is the danger and is itself unusable in application code — the shape already at Constraint: a hierarchy rule must be scoped to families where the supertype is itself unusable in application code ( Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| "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", | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-24: three of the 80 entries name classes that do not exist on any JDK, so those gates can never fire. Both consumers are pure string gates — This one is the LDAP JNDI context: the package Witness: Not claimed: a weaponised outcome.
Suggested change
and likewise Constraint, measured: the obvious witness does not work as first written. A platform-prefix "must resolve" assertion excluding only Acceptance: that platform-prefix assertion, red with the submitted spellings and green with the three corrected — and it shares its edit to — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| "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]); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-10: A repo-wide grep finds only its own javadoc ( Witness: So the concrete victim is a short-lived process — CLI tool, batch job, cron entry, FaaS invocation, a Maven/Gradle/surefire fork — that parses one document and pays ~1.9 s of exit latency (~0.4 s headless) plus two AWT threads and the platform toolkit. On a real application classpath the remaining third-party entries (Spring, Tomcat, Hibernate, logback, c3p0, Groovy, javassist, OGNL, FreeMarker, Kafka) are initialized too, which means a JSON library decides when the app's logging and container classes initialize. Delete Three further hazards are reasoned rather than demonstrated, so please treat them as risk and not as measured fact: Constraint: Acceptance: N/A for the deletion — no test references the array, and — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| } 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: | ||||||
| * <ol> | ||||||
| * <li>Subclasses of {@link ClassLoader} — they can load arbitrary code.</li> | ||||||
| * <li>Subclasses of {@code javax.sql.DataSource} / {@code javax.sql.RowSet} — classic JNDI | ||||||
| * gadget sinks.</li> | ||||||
| * <li>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.</li> | ||||||
| * </ol> | ||||||
| * 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) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-11: this null guard is unreachable. All five current call sites happen to pre-guard ( Witness: Hoist the guard to be the first statement of the method, or delete it and record the non-null contract on the public static boolean isAutoTypeDenyClass(Class<?> type) {
if (type == null) {
return false;
}
if (ClassLoader.class.isAssignableFrom(type) || isSQLDataSourceOrRowSet(type)) {
return true;
}Constraint: Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| 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])) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-17:
Witness: Build the lookup once at class init and use it at both call sites — this is the same helper the cross-package build break needs: private static final Set<String> AUTO_TYPE_DENY_FQCN_SET =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AUTO_TYPE_DENY_FQCN)));
// isAutoTypeDenyClass: return AUTO_TYPE_DENY_FQCN_SET.contains(type.getName());
// ObjectReaderImplClass: if (JDKUtils.isAutoTypeDenyName(className)) { throw ... }
One correction for accuracy: only 2 of the 5 call sites are genuinely per- Constraint: matching must stay exact-FQCN per the javadoc this diff adds at Acceptance: — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||||||
| return true; | ||||||
| } | ||||||
| } | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| public static void setReflectErrorLast(Throwable error) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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\"}"; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-12: both tests in this file pass with the two guards they were written to witness deleted, and the javadoc above states a counterfactual that is measurably false. The payload shape never reaches the guards. Witness: Drive the reader with the input shape it actually parses — the bare class-name string — and assert on the message so the test cannot be satisfied by the unrelated String payload = "\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"";
JSONException e = assertThrows(JSONException.class,
() -> JSON.parseObject(payload, Class.class, JSONReader.Feature.SupportClassForName));
assertTrue(e.getMessage().contains("autoType is not support"), e.getMessage());Then correct the javadoc to describe the measured pre-change behaviour. It is also worth covering the only configuration in which the post- Constraint: Acceptance: the rewritten — qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3) |
||
| 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) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] R1-9: [certifies-falsely] [regression] both checks added to
readObjectare unconditional — they run before theSupportClassForNamegate, ignore the provider's accept list, and veto a classcheckAutoTypehas already returned for an explicit full-name accept.That inverts the precedence the three pre-existing
isAutoTypeDenyClasscall sites encode (ObjectReaderProvider.java:952and:986both guard oni + 1 < typeNameLength) and the@returncontract this diff itself keeps atJDKUtils.java:606— "true if the class must not be resolved through a whitelist prefix match". An app that relied onSupportClassForNameto carry JDK type names in a config bean loses it with no escape hatch, while the sibling@typepath still honours the same accept entry — so one FQCN is hard-denied on one path and accepted on another. Apps that never enabledSupportClassForNamealso lose the actionable diagnostic and get an autoType error for a config that is already enabled.Witness (A/B against released fastjson2-2.0.65):
Make both checks conditional on the same policy the rest of the library uses: drop the early string loop and rely on the post-resolve check, or keep an early reject but place it after the
classForNamegate and skip it when the provider has an accept entry naming the type in full. For the post-resolve check at:84-86, restrict it to the prefix-match case so an explicit full-name accept stays an opt-in.Please do not simply delete the
:84-86check: measured, it is the only gate that stops a full-name-acceptedClassLoader/DataSource/RowSetsubtype end-to-end, becauseObjectReaderProvider.java:951-954skips its veto wheni + 1 == typeNameLength.Constraint: the post-resolve check only runs when
provider.checkAutoType(...)at:76returns non-null, so removing the early loop must not also remove that call; andTypeUtils.getMapping(className)at:71-74returns before:84, so a name check placed only beforeloadClassleaves the mapping branch uncovered.AutoTypeValidationTest.java:279-289(testExactAcceptNameAllowsClassLoader) is the existing test encoding that a full-name accept is an explicit opt-in.Acceptance: a test asserting
{"clazz":"java.net.URL"}into aClass<?>field succeeds afteraddAutoTypeAccept("java.net.URL")(red today), a mirror case asserting a full-name-accepted test-localClassLoadersubclass still throws, and{"clazz":"...TemplatesImpl"}still throws so the fix is not read as "delete the list".— qwen3.8-max-2026-09-02 via Qwen Code /review (v0.22.3)