Vulnerability Research

When Class Bytes Vouch for Themselves: Fastjson @JSONType, Spring Boot Loader, and JDK 8 Class Initialization

SOSEC showed how a JSON type value could bring a class from outside the ordinary application classpath into the JVM on Fastjson 1.2.83 , Temurin 8u492 and two Spring Boot loader generations: checkAutoType() trusted @JSONType bytes before compatibility checks, so a marker preceded ClassCastException ; the first local hit was 1.2.48 , and exposure depends on entry, loader/TCCL, JDK, SafeMode, handlers and egress.

A warm hand-drawn investigation scene connects a JSON label, nested jar container, loopback resource parcel, and class-loading gate to show the conditional path formed by Fastjson @JSONType probing and Spring Boot fat-jar loading on Temurin 8u492.
In this article

Research basisSOSEC Vulnerability Research · public disclosures, Fastjson 1.2.83 commit 26f13f84fdd5 , official JAR SHA-256 641A4D65AB32 , and a harmless marker comparison on Temurin JDK8u492 · Last revised Jul 22, 2026

SourceKirill Firsov public posts / Fastjson 1.2.83 pinned source / pinned Maven Central artifact / SOSEC Temurin JDK8u492 marker-only local matrix

1 The marker appeared before the type error

The typed-DTO run ended with the exception Java was supposed to throw: ClassCastException. Read only from the HTTP response or the final stack frame, the request looked safely rejected. Fastjson had failed to return the Dto that the caller requested. The temporary directory told a different story. A file named PWNED2 already existed, and the loopback resource service had received a request before the cast failed. The selected class did nothing else. Its initializer did not launch a process, execute an operating-system command, open a shell, or contact a system outside the test. The order is the finding: the JVM had defined and initialized a class that was not on the ordinary application classpath before the caller learned that the object had the wrong type.

That small file moved the investigation beyond a shell video. With the official Fastjson 1.2.83 JAR on Temurin OpenJDK 1.8.0_492-b09, the tested Spring Boot loader and JDK URL-handling path first retrieved a candidate from a loopback-only service. Fastjson found a runtime-visible @JSONType annotation in those bytes. The JVM accepted the class definition, and ordinary object creation triggered initialization. After changing only the effective loader to the JDK AppClassLoader, the sanitized record showed no observed loopback callback and no marker; it did not preserve an authoritative zero request count. Enabling built-in SafeMode on the tested release, with no custom handler returning first, stopped the same path before resource lookup. The successful run shows that the listed conditions were sufficient to reach class initialization; the controls show where that sequence can be interrupted.

The run used the upstream 1.2.83 tag at commit 26f13f84fdd522de10678e43f55fde918ab7b347. The Maven Central JAR had SHA-256 641A4D65AB32FBFDCCD9C718E3F83EBC4CAABDB5E4FE5B3D51527C5FE692631D. The successful loader controls used Spring Boot 2.7.18 and 1.5.22.RELEASE. Those are not decorative setup details. The same Maven coordinate behaves differently under an ordinary application loader, a Boot-defined Fastjson class with an outer thread-context loader, and an explicit Boot default loader. Any production conclusion has to name the running bytes and the loader attached to the actual parsing lane, not merely the dependency declared by the build.

SOSEC pinned every dependency, confined all externally observable effects to loopback HTTP and one inert marker, and added controls for ordinary classpath loading, SafeMode, typed parsing, two Boot loader generations, TCCL changes, sampled Fastjson releases, and explicit AutoType. Pinned source lines, the observable event sequence, and positive and negative results now form a testable chain of evidence. The sections that follow split one apparent “success” into four state changes, then trace each change back to the code that actually ran.

1.1 Separate one “success” into four observable events

On July 19, Kirill Firsov's first post described a “gadget-free RCE” in Fastjson 1.2.83. A short video moved from a loopback parsing endpoint to an interactive root shell. Follow-up posts gave a 1.2.681.2.83 range and recommended SafeMode or migration to Fastjson2. The video justified an urgent investigation, but it did not show the request body, endpoint source, Fastjson digest, loader, TCCL, parser features, or call stack. The root prompt identifies the process inside the demonstration container; it does not, by itself, show privilege escalation, container escape, or Internet reachability.

What the investigation needed was not another successful screen recording but one causal trace from input to consequence: which parsing entry accepted the type value, which branch ran inside checkAutoType(), which loader interpreted the resource name as a retrievable location, where the JVM defined and initialized the class, and whether the final exception came before or after the side effect. Locking those questions to one process, one artifact set, and one run is the only way to distinguish external retrieval, completed class definition, and initialization across the execution boundary. SOSEC therefore treated the public demonstration as the claim to investigate, not as an explanation of its own result.

The laboratory therefore recorded four events separately. A request at the loopback service proves that class-resource resolution crossed into network access; it supports an external-fetch or SSRF finding, not class execution. A successful definition proves that a loader and JVM accepted the candidate. The marker proves that class initialization ran. The final Fastjson return or exception describes only what the caller saw afterward. Each clean JVM began with no marker and a zeroed request counter. This separation prevents two opposite mistakes: calling every DNS or HTTP hit an RCE, and treating a late parser exception as proof that the earlier stages did nothing.

The controls then changed one meaningful condition at a time. With plain AppClassLoader, the sanitized record showed no observed loopback callback and no marker, but supplied no exact request count. Spring Boot 2.7.18 or 1.5.22.RELEASE used as the explicit ParserConfig.defaultClassLoader produced the request, definition, initialization, and marker. When Fastjson itself was defined by the Boot loader but the parsing thread retained the outer launcher's TCCL, resource retrieval still happened while initialization did not. Giving that thread the Boot TCCL completed the remaining steps. “Packaged as a fat JAR” is therefore only a discovery clue. The tested RCE path requires compatible loader reachability at both the resource and class-loading stages.

1.2 Source and experiment part ways at the same early return

The source explains why the marker can beat the exception. In Fastjson 1.2.83, checkAutoType() rewrites the candidate type name as a .class resource and asks a loader for its bytes. The ASM reader looks for a runtime-visible @JSONType annotation. If it finds one, jsonType becomes true, TypeUtils.loadClass() is called, and the loaded class is returned immediately. Dangerous-parent checks and expectClass.isAssignableFrom(clazz) are below that return. The problem is not simply a missing blacklist entry. Bytes that have not yet earned trust are allowed to present their own admission badge.

1.3 Code and response: the faulty lines and the doors to close tonight

Vulnerable function and branch. The reproduced fault is in ParserConfig.checkAutoType() in Fastjson 1.2.83, commit 26f13f84fdd522de10678e43f55fde918ab7b347, lines 1479–1528. Lines 1479–1493 rewrite the submitted name as a .class resource, obtain a stream through the configured loader or the loader that defined ParserConfig, and scan for runtime-visible @JSONType. Lines 1500–1510 load and immediately return the class when jsonType is true. Dangerous-parent and expectClass.isAssignableFrom() checks begin only at lines 1513–1528. The permanent source fix has to reverse that trust-before-check order.

Versions and fix status. In the same Temurin JDK 1.8.0_492-b09, Spring Boot 2.7.18, explicit Boot default-loader, marker-only setup, sampled releases 1.2.48, 1.2.66, 1.2.67, 1.2.68, 1.2.80, and 1.2.83 all initialized the class. Not every intermediate release was executed, and not every deployment has the same loader, JDK, and network conditions. The public-source review completed on 20 July 2026 found no later fixed Fastjson 1.x release or maintainer patch; the 1.x repository is archived. Fastjson2 is the public migration direction, but native APIs, compatibility layers, and application-specific type features need separate acceptance before anyone labels the service fixed.

Permanent repair is required. A fixed DTO, WAF rule, loader change, or one negative canary does not make an archived parser a durable trust boundary. Remove Fastjson 1.x from untrusted parsing, migrate to a maintained implementation after semantic and security regression, or adopt a future maintainer repair that demonstrably moves policy ahead of the dangerous return. Acceptance must inspect the bytes loaded at runtime and remove old JARs, shaded copies, plugin copies, and rollback images that could put 1.x back on the lane.

What to do tonight. On releases 1.2.68 and later that actually support SafeMode, set -Dfastjson.parser.safeMode=true through controlled deployment, restart every affected instance, and review every AutoTypeCheckHandler, setAutoTypeSupport(true), and broad accept rule. In SOSEC's handler-free 1.2.83 control, SafeMode rejected before resource retrieval; a handler decides trust before that built-in gate, while explicitly enabling AutoType still produced the marker, so both states require separate audit. Earlier releases need untrusted routes disabled or moved to another parser rather than a nonexistent SafeMode setting. Restrict unnecessary HTTP, object-storage, and artifact access from the JVM. Find ParserConfig.setDefaultClassLoader() calls and record the real TCCL on request, asynchronous, scheduled, and message-consumer threads. A gateway rule can temporarily reject type metadata outside the protocol, but it is a speed bump, not repair.

How to verify and investigate. Record the CodeSource and SHA-256 of the bytes defining ParserConfig, effective SafeMode and AutoType state, default loader, parsing-thread TCCL, full JDK build, Spring Boot loader version, and egress policy. Join unexpected @type or custom typeKey use with unusual jar or HTTP retrieval, class definitions from unexpected sources, and file, process, or credential activity on the same request and process. A network read alone is an external-fetch or SSRF finding. Class initialization or a host-side effect raises the case to the execution boundary.

A safe laboratory check needs only the production-equivalent JAR and loader arrangement, a loopback-only resource service, and an inert marker class in a disposable environment. Run paired negatives with plain AppClassLoader, built-in SafeMode, and a candidate without runtime-visible @JSONType. A passing containment result means no retrieval, no class definition, no marker, and no regression in legitimate business samples. Production verification should prefer configuration and telemetry evidence; there is no reason to send candidate class bytes to a live service.

2 Four components read the same candidate four different ways

Traditional Fastjson RCE analysis starts with the dependency tree. Is a dangerous class already present on the target classpath? Can its constructor, setter, or factory method be reached from attacker-controlled type metadata? This route changes that first question. The class that writes the marker is not installed as an ordinary application class. It begins as untrusted type text. Fastjson rewrites the text as a class resource; Spring Boot handler fallback and JDK URL rules give that resource jar-like routing meaning; the returned bytes present their own @JSONType annotation; and Temurin 8u492 accepts the class before normal object creation initializes it.

“No preinstalled classpath gadget” removes an old prerequisite; it does not remove the class, bytecode, loader, or JDK from the story. The external class is the executable unit. The tested Boot/JDK path delivers it. Fastjson's annotation branch admits it. Class initialization gives it a chance to run. Those stages belong to different components and leave different traces. A software-composition scan cannot stop after failing to find a familiar gadget, and a network alert cannot turn one HTTP request into proof of class execution.

A sealed JSON envelope is examined as a class resource, passed through nested archive jars, and delivered toward a policy gate with one barred branch, illustrating that parser trust and loader interpretation form one chain.
Figure 1. Fastjson rewrites and scans the type text; the tested Boot handler fallback and JDK URL path resolve the loopback candidate resource. Only when resource retrieval, annotation admission, class definition, and initialization all occur in sequence does the SSRF-level fetch become the RCE-grade chain verified here on Temurin 8u492.

2.1 The loader gives the resource name a second grammar

Fastjson builds the probe mechanically: replace dots in the submitted type name with slashes, then append .class. Under a plain application loader, the result normally names a file beneath a classpath directory or archive. The ClassLoader contract does not say that every implementation must treat the name as an inert local path. Once attacker-shaped text reaches getResourceAsStream(), the receiving loader also interprets it, using rules Fastjson's policy code does not model.

The controlled harness exercises exactly that semantic gap without requiring Fastjson to contain an HTTP client. Its generated class uses a bytecode-only internal name chosen so that the Fastjson transformation yields a resource string the loader and URL-handling path can treat as jar-like routing syntax. The class cannot be represented faithfully as an ordinary Java source declaration, which matters because source-language validation is not the JVM's only naming boundary. Fastjson sees a type string and a candidate .class resource. The loader path sees archive-routing syntax embedded in that resource. The loopback service returns a jar-like object containing class bytes whose internal name matches what the loader later attempts to define. The JVM on the reproduced JDK 8 build accepts that name. Each component is behaving according to a local assumption; their assumptions are incompatible when the input is attacker-controlled.

The safe canary confirmed both halves rather than inferring one from the other. Resource lookup through the Boot loader and URL-handling path returned loopback-served bytes and produced an HTTP hit. Invoking checkAutoType() with the same effective loader caused the annotation scan, and later parsing caused class initialization. Under the plain AppClassLoader, the sanitized record showed no observed loopback callback and no marker; it did not establish an exact zero request count. That distinction is operationally useful because a process may exhibit only the first half. In the run where Fastjson was defined by the Boot loader but the thread context remained outside it, the annotation probe could still make the network request through ParserConfig.class.getClassLoader(), while the subsequent TypeUtils.loadClass() path did not define the class. Such a process remains exposed to an SSRF-like resource fetch even when the full marker route does not complete.

2.2 @JSONType becomes a permission signal before the class is trusted

@JSONType is a legitimate Fastjson annotation used to describe serialization and deserialization behavior for application classes. Its presence is not malicious, and scanning class metadata without initializing the class can be a reasonable optimization. The security problem is the meaning assigned to the result inside checkAutoType(). The 1.2.83 implementation does not merely use the annotation to select a serializer after a class has passed policy. It reads the candidate bytes first, sets jsonType=true when the expected descriptor appears in RuntimeVisibleAnnotations, permits TypeUtils.loadClass() on that basis, and returns the resulting class before later compatibility checks. In effect, bytes obtained through a not-yet-bounded resource lookup carry their own admission credential.

The annotation scanner makes this ordering explicit. ClassReader lines 90–140 locate the RuntimeVisibleAnnotations attribute and pass each descriptor to a visitor. TypeCollector lines 71–74 and 106–108 reduce that stream to a Boolean recording whether the descriptor equals Fastjson's JSONType descriptor. The scan does not establish who produced the bytes, whether they came from the application's signed build, whether the resource's declared internal name is appropriate for the application, whether the candidate is assignable to the expected DTO, or whether defining it would cross a network boundary. Yet the Boolean is consumed as if those questions had already been answered.

This is why describing the route as an “annotation bypass” is incomplete. The annotation is not merely a string that skips one blacklist comparison. It is a trust channel that moves evidence from a byte stream into a class-loading authorization decision. A safe ordering would first constrain the namespace and source of the resource, prove the candidate's relationship to the expected type, apply all denied-parent and configuration policy, and only then allow annotation metadata to influence deserialization behavior. In 1.2.83 the trust flows in the opposite direction: resource bytes nominate themselves through @JSONType, the parser loads the class, and the jsonType return prevents the later checks from evaluating the loaded class at all.

2.3 Spring Boot's classic fat-JAR loader is an active participant

A Spring Boot executable JAR is not a ZIP file placed on a conventional flat classpath. Classic Boot launchers need their own archive and URL handling so BOOT-INF/classes and dependency JARs can remain nested inside the executable. That behavior is normal. The unsafe composition begins when Fastjson hands this machinery a resource name derived from untrusted type text without first ensuring that the name stays inside an approved local namespace.

Spring Boot 2.7.18 points to commit 0c8b382d42db22b92efcf47000d0ff9ef4971629. LaunchedURLClassLoader.java lines 93–156 mainly delegate resource lookup and ordinary loading to URLClassLoader. When Boot cannot open a jar URL, Handler.java lines 86–121 fall back to the platform handler. The same file's lines 366–393 show that Boot handles only file: roots itself. Once a JarFile root exists, JarURLConnection.java lines 244–267 walk the nested !/ segments inside it. The loopback retrieval therefore came from Boot's fallback composing with JDK URL handling. LaunchedURLClassLoader was part of the route, but it was not acting as a standalone remote downloader.

Spring Boot 1.5.22.RELEASE, at commit 34c62cc05919259cf75b925f88465638d1744b75, divides the work in the same way. LaunchedURLClassLoader.java lines 55–99 delegate resource lookup. Handler.java lines 92–115 provide the fallback, while lines 302–330 constrain the roots Boot handles. After a local root exists, JarURLConnection.java lines 250–274 traverse its nested segments. Both loader generations completed the marker control. That makes the finding more than a 2.7.18 one-off, but two samples still do not establish every Boot version, launch mode, repackaging variant, or newer loader design.

The loader therefore belongs in the causal sentence, not in an “environment” footnote. Fastjson creates and trusts the resource probe. The active Boot/JDK URL-handling path resolves the loopback-served jar-like candidate, and the JVM defines and initializes the resulting class. Changing any of those transitions changed the laboratory result. The two Boot samples span widely separated classic loader generations, but they say nothing universal about every Spring Boot release, launcher, repackaging plugin, container arrangement, or modern loader redesign. Source establishes which component owns each step; the loopback receipt shows that those steps composed on the tested JVM. Production inventory should record the loaded loader class and implementation version instead of guessing from the declared Spring Boot parent.

This distinction also prevents a misleading shortcut. Unpacking a fat JAR, changing the launcher, or replacing the context loader may interrupt the reproduced route, but it does not repair Fastjson's annotation-as-trust ordering. Conversely, replacing Fastjson while leaving a permissive network-capable loader may close this route while other application code still passes untrusted resource names to that loader. The permanent fix for this defect is to remove the vulnerable parser path. Separately, class and resource lookup should be designed so that an untrusted name cannot quietly become an outbound fetch.

2.4 Different threads in one service can stop at different stages

A Java service often has several loaders at once: the application loader, a Spring Boot launcher, a container loader, plugin loaders, framework-created children, and the thread context class loader, or TCCL. Fastjson adds an explicit defaultClassLoader to that list. Its resource probe prefers the explicit default and otherwise uses the loader that defined ParserConfig. The later TypeUtils.loadClass() attempt starts with the explicit loader, then tries the current thread's TCCL, and finally falls back to Class.forName. The resource stage and the definition stage can therefore see different loaders inside the same process.

The paired fat-loader control made that relationship visible. With Fastjson itself loaded by LaunchedURLClassLoader, the resource scan could enter the Boot/JDK URL-handling path because ParserConfig.class.getClassLoader() was already the Boot loader. But if the launching thread retained an outer TCCL and no explicit default loader was set, the later load attempts did not reproduce initialization. Assigning the Boot loader to the same thread's context changed the result to marker creation. Setting it explicitly as ParserConfig.defaultClassLoader also completed the route. Thus “Fastjson is inside a Boot fat JAR” is a useful discovery clue, while “the parsing lane can reach the Boot loader at both resource and class-load stages” is the exposure condition supported by the canary.

Real services complicate that condition through executor reuse. A synchronous controller may inherit the launcher TCCL; a managed servlet thread may carry a container loader; an application-created executor may capture or overwrite the application loader; a messaging consumer may be created before framework initialization; and an asynchronous callback may run in a common pool with different context. A startup observation is not enough. Inventory the threads that actually parse untrusted JSON, including scheduled work, queue consumers, file importers, RPC adapters, and error-retry lanes. Record loader identities in a safe diagnostic build or bounded telemetry, and compare them with the parser's explicit default. The same deployed binary can contain one exploitable lane, one SSRF-only lane, and one rejecting lane depending on those runtime assignments.

For defenders, the practical change is that a software-composition scan cannot close the issue by failing to find a known gadget library. The important inventory now includes the Fastjson artifact, the parser entry point, whether type metadata is recognized, SafeMode and handler state, the effective resource and class loaders, JDK behavior, and outbound reachability. An application with a minimal classpath can still meet the reproduced conditions because the selected class is delivered through the loader path. An application with hundreds of dependencies may reject the route if SafeMode owns the decision, the parsing thread cannot reach the Boot loader, the process has no egress, or the JDK refuses the unusual class name. Neither classpath size nor a generic “AutoType disabled” flag is a reliable verdict.

The term also changes detection priorities. Historical Fastjson hunts often search for known gadget class names in @type values or for side effects associated with a small library family. That remains useful for older attacks but is brittle here because the executable class need not be a familiar dependency and the dangerous string is being interpreted across two grammars. Stronger detection correlates semantics: an externally influenced type key, a class-resource lookup containing archive-routing features, outbound retrieval by a Java worker that does not normally fetch code or archives, definition by a Boot loader, and immediate initialization or filesystem/process effects. Production rules should focus on abnormal resource-fetch behavior and parser-adjacent egress instead of depending on exact strings that can vary across input and loader grammars.

Finally, the composition changes how responsibility is assigned. The unsafe trust ordering is in the archived Fastjson 1.x source; normal Spring Boot nested-JAR support is a prerequisite and hardening surface, not a standalone vulnerability. Application owners decide which loader Fastjson receives, whether untrusted JSON can carry type metadata, whether SafeMode and custom handlers are configured, and whether the process can reach arbitrary network destinations. Platform owners control JDK and packaging choices; network owners control egress. A useful remediation plan names each boundary without allowing several owners to dissolve the decision: any service that retains Fastjson 1.x on untrusted input remains on a migration clock even if one environmental prerequisite is temporarily removed.

3 The compatibility check arrived after the class did

The reproduced effect is best understood as one continuous call path rather than a list of suspicious functions. An untyped parse and a typed JavaBean parse enter different deserialization code, but both can present an attacker-influenced type name to ParserConfig.checkAutoType(). That function performs the resource probe, promotes runtime-visible @JSONType to a load decision, and returns before the checks that would normally constrain the result. TypeUtils.loadClass() then chooses among the explicit loader, the current thread context loader, and a final Java fallback. Once a Class is returned, normal Fastjson deserializers create and populate an instance. The JVM initializes the class as part of its first active use. In a typed-root call, the returned object may be rejected only after these events when the caller attempts to treat it as the declared DTO.

That ordering identifies the root cause without mislabeling ordinary downstream code as independently vulnerable. DefaultJSONParser and JavaBeanDeserializer expose the type-selection entry. checkAutoType() contains the flawed trust and early-return decision. TypeUtils.loadClass() reveals why loader state matters. Instance creation and field assignment show how a selected class becomes active during otherwise normal materialization. The security fix must dominate the load and return, not merely add another check after object creation. For investigation, however, each stage is valuable because it supplies an observable milestone that distinguishes blocked resource lookup, SSRF-only behavior, class loading without initialization, successful initialization, and late type rejection.

3.1 Two parsing doors lead to the same policy function

In a generic call such as JSON.parse(...), an object begins without a caller-supplied concrete Java type. DefaultJSONParser.java lines 315–406 handle the special default type key while parsing the object. Unless DisableSpecialKeyDetect or IgnoreAutoType changes that behavior, the parser reads the type-name string. It recognizes a few ordinary map classes directly, rejects a numeric-only value from this resolution path, and otherwise calls config.checkAutoType(typeName, null, lexer.getFeatures()) at lines 342–344. The null expected class is significant: the generic route asks policy to choose a type without an assignability target supplied by the caller.

If checkAutoType() returns no class, the parser can preserve the type key as data and continue with a map. If it returns a class, the parser changes interpretation. For an object that ends immediately, lines 352–374 obtain the deserializer, attempt a cast of any accumulated object state, and may call clazz.newInstance(). For an object with existing fields, lines 389–393 cast the accumulated map to the selected class and parse remaining content into it. Otherwise lines 396–405 obtain the class's deserializer and delegate the rest of the stream. These branches explain why a returned Class is more than metadata: it redirects the parser from building a neutral JSON object into materializing a Java type selected through checkAutoType().

The laboratory generic run followed this route far enough to create the benign marker and return an object. The marker cannot be attributed simply to reading the annotation. The ClassReader scan is designed to inspect bytes without initializing them. Nor does ClassLoader.loadClass(), by itself, guarantee initialization. The effect appears when the selected class reaches active use during parse materialization—whether through reflective instantiation or an equivalent deserializer construction path. This distinction matters to instrumentation. A class-load event can precede the marker, and a resource-fetch event can precede the class load. Defenders should preserve timestamps and thread identity across all three rather than assume that one event is a proxy for the others.

The generic entry also clarifies the limited role of input filters. Blocking the conventional type key before Fastjson reaches this branch can reduce exposure for endpoints that use the default key and do not have alternate typed routes. It does not repair checkAutoType(), and text matching must survive JSON decoding, character normalization, content transformations, and application wrappers. The source already supports explicit feature flags that change special-key handling, but a global toggle can break legitimate polymorphism or leave other entry points. An ingress rule should therefore be treated as temporary reduction backed by parser-level verification. The permanent decision remains removal of a branch that lets untrusted resource bytes authorize their own class loading.

A typed call looks safer because the application supplies Dto.class, and in many parsers that root type closes polymorphic selection unless explicitly reopened. Fastjson's JavaBean deserializer retains a type-key path. JavaBeanDeserializer.java lines 786–838 compare the current key with the bean's configured type key or the default type key. When the value does not simply repeat the declared bean type, the code first tries seeAlso mappings and an optional deserializer-specific handler. If those do not supply a type, line 823 calls config.checkAutoType(typeName, expectClass, lexer.getFeatures()), where expectClass is derived from the caller's declared type.

Under an ordinary policy outcome, that expected class should be a hard boundary: any alternative must be assignable to the DTO before it can be deserialized. The 1.2.83 implementation calculates an expectClassFlag and uses the expected type in several allow, deny, mapping, and final compatibility branches. But the jsonType return at lines 1505–1510 ignores the relation. Once the resource scan has set the flag and TypeUtils.loadClass() returns a class, checkAutoType() exits. The assignability check at lines 1520–1528 is unreachable for that class. Back in JavaBeanDeserializer, line 825 obtains a deserializer for the returned type, and line 828 materializes it. The expected DTO survives only as the generic return declaration, not as a pre-construction enforcement point.

This is the exact source explanation for the typed marker result. The external candidate class was not assignable to the local Dto, yet its initialization occurred before the caller-visible ClassCastException. If the parser had applied expectClass.isAssignableFrom(clazz) before returning the class, the route would have stopped before a deserializer could actively use it. If it had constrained the resource source before scanning, the external bytes would not have supplied the permission signal. The fact that a later cast correctly rejects the object is not a mitigating control; it is evidence that compatibility was enforced in the wrong temporal position.

A locked typed document leads to a jar of objects; one object exits through an open door and leaves a green marker before a red crossed barrier rejects it, showing an effect before the final cast failure.
Figure 2. The fixed root type does not protect the pre-cast interval: the @JSONType early return lets the external class materialize, and the reproduced marker records initialization before the caller rejects the incompatible result.

Application reviewers should follow this path from the public API all the way to the call site. Identify the exact JSON.parseObject overload, the root and nested types, any @JSONType(typeKey=...) declarations, seeAlso relationships, parser features, custom deserializers, and what the caller does with both the result and exceptions. A controller signature alone cannot establish safety. A fixed root may contain polymorphic nested values; a bean annotation may define an alternate type key; a wrapper may catch ClassCastException and convert it to an ordinary 400 response; and an asynchronous parse may run under a different TCCL. The source coordinate supplies the policy junction, while the application call graph supplies reachability.

3.2 checkAutoType() reads the badge before it checks the bearer

Within checkAutoType(), SafeMode is evaluated early at lines 1325–1331, after registered global handlers but before the name hashes and resource probe. That order explains the clean SafeMode control: with no handler returning a class, the function throws before it reconstructs a resource, so the loopback server sees no request. The function then derives effective AutoType support, applies length and name-shape checks, calculates hashes, considers internal allow and deny sets, and consults existing mappings and deserializers. Those earlier branches matter to other AutoType behaviors, but the reproduced candidate reaches the later annotation-probe block with autoTypeSupport=false and without a pre-existing mapped class.

At lines 1479–1498, the function initializes jsonType to false and constructs the candidate resource by replacing dots with slashes and appending .class. It calls defaultClassLoader.getResourceAsStream(resource) when an explicit loader exists; otherwise it calls ParserConfig.class.getClassLoader().getResourceAsStream(resource). If a stream is returned, ClassReader scans it and TypeCollector.hasJsonType() supplies the Boolean. Exceptions are swallowed and the stream is closed. The absence of a propagated fetch error can make network activity look like an ordinary later AutoType rejection, which is why egress telemetry is necessary even when the application returns only a parser exception.

At lines 1500–1510, the Boolean changes the state. The disjunction autoTypeSupport || jsonType || expectClassFlag authorizes TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass). When jsonType is true, the class may also be cached, and a non-null result is returned immediately. The code does not record provenance tying the loaded class to an application-owned archive, compare the class's source with the stream that was scanned, or re-evaluate whether the loader interpreted the resource name through a network scheme. More directly, it does not apply the policy checks immediately below.

Lines 1513–1528 would reject classes derived from ClassLoader, DataSource, or RowSet and would enforce assignability to expectClass. Because the jsonType branch has already returned, none of those checks constrains the reproduced class. This dominance relationship—not merely the existence of class loading—is the defect. Moving a check into documentation, adding a catch around later deserialization, or relying on the final Java cast cannot restore an invariant that the control flow has bypassed. A source repair would need to make every class returned from the annotation path pass the relevant compatibility and provenance policy before it is loaded or actively used.

The local setAutoTypeSupport(true) result is consistent with this control flow. Enabling AutoType makes the load disjunction true independently of jsonType; it does not remove the annotation branch or insert an earlier compatibility check. When the scan still finds the annotation, the same early return occurs and the marker appears. This is why an isolated statement that “the exploit works with AutoType off” should not be inverted into “turn AutoType on.” Disabled AutoType fails to block the trust channel, while enabled AutoType broadens class-selection behavior and likewise does not prevent the reproduced route. SafeMode's early throw, by contrast, changes reachability before the resource request when no earlier handler has supplied a class.

3.3 The explicit loader goes first; the worker's context gets a second try

TypeUtils.java lines 1759–1788 expose the second loader decision. When checkAutoType() passes a non-null explicit loader, TypeUtils.loadClass() tries that loader first and optionally caches the class by the supplied name. If the attempt throws, the function catches the throwable and continues. It next reads Thread.currentThread().getContextClassLoader(); when that loader is non-null and different from the explicit loader, it calls loadClass there and may cache the result. A final branch calls Class.forName(className). Failures in these branches are generally suppressed so the caller can later produce a generic AutoType decision.

The explicit-first order explains why ParserConfig.setDefaultClassLoader(fatClassLoader) completed the route even when an outer thread loader would not. The TCCL fallback explains why setting the worker's context to LaunchedURLClassLoader also completed it when no explicit default was used. The resource scan and load stages are related but not identical: the scan falls back to ParserConfig.class.getClassLoader(), whereas the load helper falls back through the current thread and then Class.forName. A Boot-defined Fastjson class can therefore fetch candidate bytes even when a mismatched TCCL prevents later definition. That split produced the laboratory's fetch-without-marker result and is the reason responders should not equate “no code execution reproduced” with “no external interaction occurred.”

Caching adds a state dimension that deserves care in testing and incident review. When the annotation is present, cacheClass is true at the 1.2.83 call site, and a successful load can be placed in Fastjson's mappings. Subsequent behavior in the same JVM may no longer exercise exactly the same resource and policy path. A laboratory matrix should therefore use a clean process for each cell or prove that parser mappings, class loaders, and generated class identities have been reset. A production investigation should retain process start time and sequence of suspicious requests; an earlier request could prime class or mapping state before a later request generates the visible effect. Such priming is not required for the reproduced chain, but stateful caches can otherwise confound comparisons.

3.4 Ordinary object creation is where the selected class comes alive

After type selection, Fastjson uses its ordinary deserialization machinery. JavaBeanDeserializer.createInstance() lines 190–283 invoke a default constructor or factory method when applicable. In the reproduced marker class, first active use triggers the JVM's static initialization before ordinary construction completes. The parser need not contain an explicit “run initializer” call; Java class lifecycle supplies that step once deserialization actively uses the loaded class.

The typed result also depends on where Java performs its concrete cast. JavaBeanDeserializer carries a generic return type T, but generics are erased in the bytecode. Returning the selected object from the deserializer therefore does not necessarily enforce Dto at that instruction. The caller that assigns or uses the result as Dto can receive the compiler-generated checkcast later, after Fastjson has selected the user type and ordinary object creation has initialized it. That makes two checks easy to confuse: expectClass.isAssignableFrom(clazz) is parser policy and should reject the class before use; the later checkcast is Java's enforcement of the caller's type expectation.

The vulnerable early return skips the parser check, while the later cast still works exactly as designed. A stack trace ending in ClassCastException proves that the value was incompatible, not that the JVM avoided every earlier effect. SOSEC bound the order with a marker absent before the run, a timestamped initialization event, and the subsequent exception. Another API wrapper may place the final cast elsewhere, so its reachability still needs a trace, but it cannot treat a late cast as compensation for a skipped parser check.

Property population is another downstream effect boundary. FieldDeserializer.setValue() lines 59–220 can invoke setters, mutate get-only collection or map contents, or assign fields reflectively. These are normal features required to build Java objects. They are not the root cause identified here, and the safe external-resource canary did not need a command-bearing setter. They do explain why choosing an untrusted class before policy is complete has consequences beyond static initialization: constructors, factories, accessors, setters, and field assignment all belong to the reachable materialization surface once the wrong type has been admitted.

The correct causal summary is therefore precise. Fastjson does not execute a shell string hidden in JSON. It accepts attacker-shaped type metadata, asks a loader for corresponding class-resource bytes, treats a runtime-visible Fastjson annotation in those bytes as trust, loads the candidate through the explicit or contextual loader path, returns it before compatibility checks, and hands it to normal object materialization. On the tested Temurin 8u492 and Boot loader, that process defines and initializes the supplied class. The local class writes only PWNED2, but the ability to initialize supplied bytecode is the RCE-equivalent primitive. Defense and repair acceptance do not depend on a reusable attack string: pinned source, canary hashes, retrieval counts, loader records, and positive and negative controls establish the state transition and its stopping points.

That source walk also defines a safe trace plan. Instrument or log the public parse call and request correlation; the special type-key branch; entry and return from checkAutoType(); candidate resource lookup and URL protocol; annotation-scan result; loader identity at each loadClass attempt; class definition and initialization; deserializer selection; constructor or factory invocation; field population; and final return or exception. In production, avoid logging raw hostile type values if doing so would redistribute exploit material; hash or classify them and keep exact samples in a restricted evidence store. A successful defense can be proven by the earliest blocked state—preferably SafeMode or input policy before resource lookup—while an incident investigation should follow the sequence through any late exception rather than stopping at the HTTP response code.

4 A version number names the code; the runtime decides how far it gets

Version numbers answer only one dimension of exposure. The plain-JDK-loader control's sanitized record showed no observed callback or marker, without an exact request count; a different loader arrangement produced an outbound request without initialization; and the Boot loader used explicitly or as the worker TCCL produced full marker creation. SafeMode stopped before the fetch; enabling AutoType did not. The tested Temurin 8u492 accepted the bytecode-only name in the reproduced route, while later JDKs have not undergone SOSEC's local matrix. A defensible affected statement must therefore name the Fastjson mechanism range, loader family and reachability, JDK evidence, parser mode, entry-point reachability, and network conditions rather than presenting “1.2.68–1.2.83” as a universal property of every application.

4.1 Version 1.2.48 is the source starting point for this annotation-probe branch

Source history explains why the 1.2.48 result is more than an isolated green cell. The 1.2.47 tag points to commit 03d77b6e629b6ff825a1c8cc3f7cbba7feb98395, while 1.2.48 points to 28da8e194bb7bca71676bb10b9cb343653289de6. Between them, commit 11b92d9f33119ca2af1a3fe6f474de5c1810e686, included in 1.2.48, added the ClassReader-based resource scan, the jsonType Boolean, authorization based on that Boolean, and the annotation-driven return. The follow-up commit 39ccafb821276bec50e59e9cb7617627632bdb7c refined the surrounding expectClassFlag conditions. The 1.2.47 code instead tried ordinary class loading before inspecting an annotation reflectively; bytes obtained from outside the ordinary classpath could not authorize that initial load in the same way.

This version finding corrects two possible mistakes. The first is accepting the public 1.2.68 lower bound as the date the source defect appeared. That range may describe a claimant's scenario, tested application route, or advisory policy, but it is not the introduction point of the source mechanism reproduced here. The second is stretching the 1.2.24 success forward as if one vulnerability had existed continuously. Fastjson 1.2.24 predates the restrictions introduced after earlier AutoType disclosures; its marker result belongs to the old broadly open class-selection model. Version 1.2.25 interrupts that behavior in this matrix, and the later resource-annotation trust channel must be evaluated as a distinct route with its own introduction and prerequisites.

4.2 Ten version samples separate old AutoType, resource-only effects, and initialization

Version 1.2.68 retains two distinct meanings: it is the lower bound Firsov published for his scenario, and that release line provides built-in SafeMode that can be validated. It is not the introduction point of the annotation-probe branch. SOSEC's 1.2.48 result answers which sampled release first completed this mechanism under the fixed Temurin 8u492 and Boot-loader conditions. Neither result establishes a maintainer-confirmed continuous interval, and unexecuted releases cannot be filled in from the endpoints.

The first version matrix was discarded because fastjson-1.2.83.jar remained visible on the parent classpath; Chapter 5.2 explains the failure and clean rerun. The table contains only corrected results from Temurin OpenJDK 1.8.0_492-b09, Spring Boot loader 2.7.18, an explicit Boot default loader, a loopback resource service, and the same marker-only candidate. Each cell began in a clean JVM with no marker. Request counts belong to this environment and are not a portable fingerprint.

FastjsonLocal HTTP requestsClass initializedPWNED2Interpretation in this environment
1.2.242YesPresentOlder open AutoType behavior; not attributed to the later @JSONType resource-trust mechanism.
1.2.250NoAbsentRejected before an external resource fetch in this route.
1.2.462NoAbsentResource interaction occurred, but the marker class was not initialized.
1.2.472NoAbsentResource interaction occurred; this tag does not contain the annotation byte-scan branch introduced next.
1.2.484YesPresentFirst locally verified release for the reproduced annotation-probe and Boot-loader chain.
1.2.664YesPresentReproduced.
1.2.674YesPresentReproduced.
1.2.684YesPresentReproduced; this release line also provides built-in SafeMode that can be validated.
1.2.804YesPresentReproduced.
1.2.834YesPresentReproduced against the final 1.x release and the primary source reviewed here.

Version 1.2.24 produced the same marker through an older, broadly permissive AutoType decision; it does not move the later @JSONType mechanism back to that release. Versions 1.2.46 and 1.2.47 made two loopback requests without initialization, so they remain external-fetch or SSRF findings rather than full local RCE positives. Version 1.2.48 is the first corrected full positive, followed by the sampled 1.2.66, 1.2.67, 1.2.68, 1.2.80, and 1.2.83. Procurement and scanner ranges must retain these as discrete results until the intervening releases and variants are actually tested.

4.3 Loader reachability divides rejection, SSRF-only behavior, and initialization

The loader controls establish at least three runtime states. In the first, the plain-loader control ends in AutoType rejection with no marker and no observed callback in the sanitized record; because that record lacks an exact request count, it supports an observed negative rather than a universal zero. In the second, ParserConfig.class.getClassLoader() or the explicit resource loader interprets the candidate as an external jar-like route and fetches bytes, but TypeUtils.loadClass() cannot define the class through its explicit, TCCL, or fallback choices; the process has an SSRF-like fetch without the reproduced code-execution consequence. In the third, a compatible Boot loader participates at both stages, the JDK accepts the class name, and ordinary materialization initializes the class. These are different security outcomes and should be recorded separately.

The explicit default loader is the clearest exposure signal because it is consulted in both the resource probe and the first class-load attempt. Search source and bytecode for setDefaultClassLoader, but also inspect framework integrations and runtime state: a library or application initializer may configure the global parser indirectly. Absence of that call is not a complete negative because the fallback loaders can still align. If Fastjson is itself defined by LaunchedURLClassLoader, resource lookup already uses the Boot loader. A request or worker TCCL set to the same loader can then complete the load. Conversely, a container or outer TCCL can interrupt the second stage while leaving outbound fetch exposure intact.

Inventory must be lane-specific because context loaders follow threads. For each externally influenced parse, identify whether it executes on the request thread, an application executor, a common fork-join pool, a scheduled worker, a message consumer, or a virtualized wrapper; then record both the TCCL and ParserConfig's defining and default loaders. Repeat the check after startup hooks, redeploys, and executor reconfiguration. A service that was SSRF-only in one test can become initialization-capable when a framework update changes thread-factory behavior. This is why loader manipulation is a containment measure with regression tests, not a substitute for retiring Fastjson 1.x.

Outbound network policy further divides reachability from consequence. The full canary required the Boot/JDK URL-handling path to retrieve the jar-like object from a local server. A production worker with deny-by-default egress should fail that retrieval even if parser and loader conditions align. Verify the control at the process, container, host, proxy, and network-policy layers; DNS denial alone may not stop literal-address or proxy-mediated access, and an HTTP allow list intended for business APIs may still be overly broad for a parser worker. Alert on denied attempts because a blocked fetch can be evidence of exploitation activity. Do not loosen egress to run a production test. A clean laboratory with loopback-only serving is sufficient to validate the parser and loader boundary.

All successful SOSEC marker runs used Temurin OpenJDK 1.8.0_492-b09. Separate proof-of-concept material states that the same Boot loader resource path can still retrieve on JDK 9 and later but class definition fails because the bytecode-only name is illegal under newer runtime checks. That is a useful stopping-point hypothesis, not a local result. Different class-name constructions, loader versions, test setups, or mechanisms can produce different outcomes on the same major JDK. The evidence supports RCE on Temurin 8u492 under the measured conditions; later JDKs require their own controlled results, and possible SSRF remains material wherever resource fetching occurs.

“Upgrade Java” is consequently valuable platform hygiene but not a verified permanent fix for this issue. A newer runtime may reject this exact generated name while leaving the unsafe Fastjson resource probe, outbound access, and alternative loader behavior in place. It may turn RCE into SSRF in one configuration, fail earlier in another, or remain exploitable through a different construction that has not been disclosed. Teams should upgrade unsupported JDKs on their own schedule, but closure for the Fastjson finding requires removal of 1.x from the untrusted path or a maintained source repair with a regression test. Where a newer JDK is used as immediate risk reduction, document the exact vendor build, Boot loader, exception, HTTP behavior, absence of definition and initialization, and residual fetch risk.

The JDK boundary also explains why a screenshot naming only “Java 8” is incomplete. Vendor patch level, architecture, class-loader implementation, URL handler registration, security properties, module behavior, container image, security agent, and command-line flags can all affect a result. The successful local run therefore records Temurin and the full build string 1.8.0_492-b09 beside the image and loader identities. A fleet query should preserve the same detail rather than reducing every runtime to a major-version Boolean; otherwise an apparent match may describe a materially different class-definition environment.

A safe cross-JDK validation matrix should preserve the same marker bytes and loader version while changing one runtime at a time. For each clean JVM, record whether resource lookup occurred, which URL handler and loader returned the stream, whether loadClass returned a Class, whether definition failed and with what exception, whether initialization occurred, whether the marker appeared, and what the parser returned. Run it on loopback in an isolated environment with no command-bearing initializer, retaining hashes, logs, and the condition matrix. Such evidence can determine whether a newer JDK blocks definition, merely shifts the outcome to SSRF, or changes nothing for a given packaged service.

4.4 Say which step is proven instead of hiding the conditions in a footnote

A service is a high-priority candidate for the reproduced route when all of the following facts converge: an affected Fastjson 1.x artifact containing the pre-load @JSONType probe is executing; untrusted data reaches a generic or typed route that recognizes type metadata; built-in SafeMode does not stop the decision and no earlier handler state has been safely adjudicated; the resource probe can reach a Spring Boot classic fat-JAR loader or another loader with equivalent jar-style external resolution; the explicit default loader or parsing thread TCCL can define the selected class; the JDK accepts the resulting binary name; and outbound access can retrieve the resource. The consequence then depends on the privileges and isolation of the Java worker, but attacker-supplied class initialization is already an RCE-equivalent primitive.

A service should not be declared unaffected merely because one gate appears absent in a manifest. Prove the running artifact rather than the build file, the live parser configuration rather than a default assumption, the actual worker loader rather than the main thread's loader, and enforced egress rather than an architectural diagram. Equally, do not declare confirmed RCE from the dependency version alone. If the loader cannot fetch, the path is blocked at retrieval; if it fetches but cannot define, the observed consequence may be SSRF; if SafeMode throws before lookup, the route is blocked at policy; if the entry never recognizes external type metadata, it is not reachable through that call. Each negative should include its evidence and expiry condition so a deployment change does not silently convert it into permanent assurance.

The immediate operational classification can therefore use four outcomes. “Not present” means the exact running process does not contain Fastjson 1.x, including old shaded, repackaged, and plugin copies. “Present, route not reachable” means the application call graph and bounded runtime evidence show that untrusted input cannot reach type resolution, with an owner and regression test. “Present, route interrupted” means a necessary environmental gate such as SafeMode or egress blocks the tested mechanism, but migration remains open. “Present and exposed” means the parser, loader, JDK, and network conditions align or cannot be disproved; containment and incident-oriented telemetry take priority. A fifth state, “unresolved,” is more honest than converting missing loader or thread data into safety.

Fastjson 1.x is archived, no fixed 1.x version for this route has been identified, and older releases carry separate AutoType risks. SafeMode, strict handlers, input policy, loader changes, and egress controls can interrupt the path and buy time. They do not restore maintainership or remove the vulnerable control flow. The lasting fix is to replace the dependency, test both application behavior and parser security in the exact replacement mode, remove dormant copies, and keep a regression test that fails if attacker-controlled type metadata ever causes a class-resource network lookup again.

The source, loader behavior, and corrected version results now tell one connected story. Before carrying it into production, however, the experiment must show how it ruled out a stale marker, the wrong JAR, and a test class that was already visible. Without those controls, a convincing screenshot can still be caused by the wrong thing. Chapter 5 returns to the bench to explain what each control removed—and why the first version table had to be thrown away in full.

5 The useful part of the reproduction was not the marker on screen

Chapter 1 pins the Fastjson JAR and source commit, Temurin build, and two Boot loader generations. Each evidence record binds those identities to one run: one process, one defining source for ParserConfig, one loader arrangement, a loopback counter reset to zero, and a marker absent before launch. Firsov's public material proposes the claim to test; SOSEC's independent controls establish the artifact identities, logs, and runtime observations.

Two canary families answered different questions. The visible-class canary was already on the application classpath. It showed that Fastjson could treat @JSONType as permission while AutoType was disabled, select the class, and let a typed root complain only after active use. The Boot-loader canary was not installed as an ordinary application class. Its bytes had to be retrieved through the loader's nested-archive semantics before they could be defined and initialized. The first experiment exposed the parser's trust order; the second showed that a compatible loader could bring previously unavailable bytes into that order. Collapsing them into one “worked” result would hide the most important question: whether the class was already present or arrived during the parse.

5.1 Eight controls removed one support at a time

Safe test rowObserved resultWhat the row provesWhat it does not prove
Ordinary JDK AppClassLoaderNo loopback callback observed in the sanitized record; no marker; parse rejected; exact request count unavailable.The reproduced route requires resource interpretation and class-loading semantics beyond this ordinary-loader control.It does not show that a conventional classpath is free of other Fastjson risks or preinstalled dangerous classes.
Boot 2.7.18 loader explicitly used as ParserConfig.defaultClassLoaderCandidate bytes were read; the class was defined and initialized; the marker appeared; parsing returned an object.The pinned Fastjson artifact, Temurin 8u492, loader, and parser conditions are sufficient to complete the harmless chain.It does not generalize to every Boot release, launcher, JDK, parser lane, or production network boundary.
Boot 1.5.22 loader under the same material conditionsCandidate bytes were read and the marker appeared after definition and initialization.The observation is not confined to the single Boot 2.7.18 loader sample.Two separated loader generations are not a complete Spring Boot version or packaging matrix.
Fastjson defined by the Boot loader while the worker TCCL remained the outer loaderA resource read occurred, but no initialization marker appeared and parsing was rejected.Resource inspection and later class definition are separable stages that can see different effective loaders.The external read alone is not proof of code execution, and marker absence does not mean no network-side effect occurred.
The parsing thread's TCCL also set to the Boot loaderThe resource read and marker both appeared; parsing returned an object.The context loader of the thread performing the parse can decide whether the route advances from retrieval to definition and initialization.The loader attached to a startup or main thread cannot establish the state of every request, executor, or message-consumer thread.
Typed parse with a fixed root Dto.classThe marker appeared before the caller received ClassCastException.For the tested path, incompatibility was enforced after the selected class had already initialized.It does not prove reachability through every DTO graph, converter, nested member, parser overload, or application wrapper.
Built-in SafeMode enabled, with no custom type-check handlerNo resource read, no marker, and rejection occurred before type selection completed.Built-in SafeMode blocked this tested handler-free route before the loader crossed the resource boundary.It does not audit other ParserConfig instances or application handlers that can make an earlier trust decision.
AutoType support explicitly enabledCandidate bytes were still read and the marker still appeared.Enabling AutoType is not a mitigation for the reproduced route.It does not establish that the AutoType setting is irrelevant to every other Fastjson path or historical vulnerability.

The evidence packet must make a stale marker or a test-fixture side effect distinguishable from target-class initialization. Before each run, the marker state and resource-service counter were reset and recorded; after the run, the packet associated the marker contents and timestamps with one process start, one parser configuration, and one set of artifact digests. Reviewable records include the Fastjson JAR digest, outer application artifact digest, JDK vendor and complete build, architecture, Boot loader identity, canary revision, Fastjson code source, explicit default loader, parsing-thread TCCL, SafeMode and AutoType state, resource log, class-loading observations, parse result, marker contents, and exception trace. A screenshot containing only PWNED2 is not sufficient. Without process, time, and artifact correlation, a reviewer cannot exclude residue from an earlier run or a marker created by fixture initialization.

5.2 Results this tidy forced SOSEC to discard the first version matrix

The first version run looked too uniform to trust. Ten launches, nominally spanning Fastjson 1.2.24 through 1.2.83, each produced four loopback requests and the same marker. The pinned 1.2.47 source did not yet contain the later pre-load @JSONType byte scan, and 1.2.25 is a transition away from the older, broadly permissive AutoType behavior. If releases separated by both changes behaved identically, the first question had to be which Fastjson classes the JVM had actually loaded.

The launch configuration supplied the answer. The parent Java classpath still contained fastjson-1.2.83.jar. Each intended release also appeared in the Boot loader's URL list, so the setup log displayed the expected filename, but that line did not prove that the matching JAR had defined ParserConfig. A parent-visible 1.2.83 copy could contaminate every nominal cell. The entire first matrix was therefore invalidated. None of its rows support the version range or the RCE result. The invalidation records a common Java testing trap: changing a loader URL, manifest, Maven property, or filename is not the same as changing the class bytes executing in the process.

The corrected run put the intended Fastjson JAR first on the Java classpath and passed that same absolute path into the harness. Every cell began in a new JVM after the old marker was removed and the loopback counter was reset. The run recorded the actual version alongside the requested version and preserved a separate log digest. Only then did the source generations separate. Version 1.2.24 made two resource requests and initialized the class through its older open AutoType behavior. Version 1.2.25 rejected before any request. Versions 1.2.46 and 1.2.47 each made two requests but never produced the marker. Version 1.2.48 was the first sample to make four requests and initialize the class through the later annotation-probe composition. The later samples 1.2.66, 1.2.67, 1.2.68, 1.2.80, and 1.2.83 did the same. The table in Chapter 4 is this corrected run, not the discarded all-green one.

The failed run fixes the evidence boundary. Version 1.2.48 is the earliest successful local sample, and the upstream annotation-scan commit is present in that tag; five later samples also succeeded. Unexecuted releases 1.2.49–1.2.65, 1.2.69–1.2.79, and 1.2.81–1.2.82 are not direct laboratory positives. Android, noneautotype, vendor backports, shaded copies, repackaged libraries, and private forks likewise require their own runtime identity. A trustworthy range comes from agreement among pinned source history, the bytes the JVM actually loaded, and discrete process results—not from drawing a line between the first and last green cells.

The same artifact should also be observed once from a cold process and once after the lane has handled data. When jsonType is true, the 1.2.83 call site permits a successfully loaded class to enter Fastjson's mappings; the JVM and loader also retain classes they have already defined. A first request may perform resource retrieval while a later request reuses process state, produces a different exception, or leaves no equivalent network trace. The version comparison therefore requires a new JVM for every cell. A production investigation should preserve process start time and the order of suspicious requests so a quiet later event is not mistaken for proof that retrieval never happened. Caching is not an additional prerequisite for this chain, but it changes which evidence remains visible. Mixing cold and warm observations can make a control appear effective or ineffective for the wrong reason.

6 Outside the lab, start with the JVM that actually parsed the data

A laboratory can pin one JAR, one thread, and one network path. Production will not volunteer those answers. A dependency scanner can show that Fastjson exists in a repository or image, but it cannot tell which ParserConfig handled a request or which TCCL that worker carried. Start with one real data path and walk it backward: who supplied the JSON, which code parsed it, which 1.x bytes the JVM loaded, whether candidate-resource lookup left the process, whether a class was defined and initialized, and whether anything else changed on the host. Skip a step and the assessment will either promote a dormant JAR to RCE or demote a late exception to safety.

The positive route needs several facts at once. Untrusted data must reach type selection. Resource inspection must land on a loader that understands the relevant nested-archive semantics. An explicit default loader or the parsing thread's TCCL must be able to continue from retrieval to definition. The tested Temurin 8u492 must accept that definition, and the worker must be able to reach the resource. SafeMode can close the door before the probe; a custom handler can make a decision before SafeMode; process authority determines the damage available after initialization. None of those facts can be inferred from pom.xml, a container file listing, or the sentence “we use Spring Boot.”

6.1 Do not begin with pom.xml; work backward from the live ParserConfig

Begin with the bytes that actually define com.alibaba.fastjson.JSON and com.alibaba.fastjson.parser.ParserConfig. Build-side discovery should cover direct and transitive dependencies, BOOT-INF/lib inside executable JARs, shaded classes, plugin directories, application-server shared libraries, vendor bundles, and compatibility modules. Runtime discovery should record code source, loaded JAR hash where policy permits, defining loader identity, process image and container digest, JVM start time, and the complete Java vendor and build string. A classic Spring Boot 1.x or 2.x LaunchedURLClassLoader, a plain AppClassLoader over an exploded deployment, a WAR container loader, and a proprietary plugin loader are different security objects even when they launch the same application version.

Duplicate copies require particular care. A dependency report may show a replacement while an old shaded copy still defines the class in one plugin; a fat JAR may contain two versions under parent and child loaders; a rolling deployment may leave a long-running queue consumer on an earlier image. The useful unit is not “the repository uses Fastjson” or “the host has Fastjson.” It is the tuple of running class bytes, defining loader, process, deployment replica, parsing lane, and observation time. A dormant file establishes inventory debt. Only the copy reached by the live call path establishes exposure for that request.

A live process can reveal that identity without repeating the vulnerability. On the actual parsing lane, record ParserConfig.class.getProtectionDomain().getCodeSource() and the class and identity of ParserConfig.class.getClassLoader(). In an isolated restart of a JDK 8 service, -verbose:class can show which location supplied a class; later JDKs offer unified class-loading logs. Where policy allows, extract the artifact from that location, hash it, and compare it with the build and image inventory. If duplicate or shaded copies are plausible, repeat the query for TypeUtils, DefaultJSONParser, and the active deserializer. They should resolve to the code generation being assessed, not merely to files with matching names.

A missing CodeSource does not prove the library is absent. Custom or container loaders, transformed bytecode, dynamically defined classes, and security policy can leave the protection-domain location null or misleading. In that case, recover provenance through approved instrumentation, a class-resource stream from the defining loader, JFR or equivalent class-load telemetry, and the loader's own archive inventory. Keep the byte hash beside process start time, image or enclosing-artifact digest, JVM arguments, defining-loader identity, and parsing-thread TCCL. When parent and child loaders can both see the same class name, record which delegation path won rather than assuming the nearest JAR on disk supplied the running bytes.

This runtime check is how migration avoids reproducing the discarded matrix in production. A new dependency can coexist with an old plugin copy. A rolling deployment can update request workers while an unrestarted consumer keeps 1.x alive. A rollback image can restore the archived parser even though the main branch is clean. First prove that the intended replacement defines every parser class on the exercised lane; only then run semantic and harmless rejection tests. File inventory says what could be loaded. Runtime identity says what handled the data. Class-loader delegation makes both necessary.

Next observe both ParserConfig.defaultClassLoader and the thread context class loader on the thread that performs each relevant parse. Controller threads, servlet pools, @Async work, CompletableFuture callbacks, common pools, scheduled jobs, queue listeners, import workers, retry executors, and plugin-created threads can carry different TCCLs. A value printed from the main thread at startup cannot stand in for them. The laboratory controls already showed why: a Boot-defined Fastjson class could perform the resource probe while a mismatched worker context prevented later initialization, whereas a compatible TCCL or explicit default loader completed the measured Temurin 8u492 path. Loader reachability is a per-lane runtime fact, not a packaging adjective.

For every lane, start from JSON.parse(), JSON.parseObject(), HTTP message converters, custom deserializers, and secondary JSONObject.toJavaObject() calls. Record the exact API and overload, declared root and nested types, ParserConfig instance, active parser features, type-key customizations, built-in SafeMode, and every global, annotation-level, or deserializer-specific AutoTypeCheckHandler. An application can have one globally hardened configuration and a separate locally constructed parser that lacks it. Because handlers run before the built-in SafeMode check in 1.2.83, the inventory must establish whether any handler can return a class and thereby make an earlier trust decision. AutoType being disabled is useful context, but the reproduced annotation route operated with autoTypeSupport=false; it is not a closing fact.

Finally, map every untrusted producer, not only public HTTP controllers. Webhooks, file upload and import, batch jobs, message brokers, cached serialized values, RPC adapters, Redis serializers, administrative tools, partner feeds, disaster-recovery replicas, and consumers that have not restarted since a configuration change can all cross a trust boundary. Authentication narrows the set of producers; it does not make tenant, partner, compromised-account, or cross-tenant data intrinsically trusted. A fixed DTO is also not negative evidence. The typed canary demonstrated that an incompatible result can be rejected after class initialization, so reachability analysis must end at the parser's type-selection decision rather than at the controller signature or final cast.

An entry can leave the exposure map only when evidence removes its type-selection opportunity. That may mean the business path is genuinely decommissioned, the running call graph no longer invokes Fastjson 1.x, or an independent strict schema gate rejects all type metadata before Fastjson and is verified against the actual decoded representation. A WAF pattern, a generic content-type rule, or a wrapper that converts parser exceptions into HTTP 400 does not establish this property. Canonicalization, alternate encodings, nested values, non-HTTP paths, and local parser configuration all sit behind those surface controls.

6.2 Every parse has two maps: how data came in and where the JVM can go

Ingress tracing should begin with the least-trusted producer and follow the data through CDN, WAF, ingress controller or reverse proxy, listener, authentication, request filters, transformation layers, and the exact parse call. The loopback endpoint used in research was only a laboratory wrapper; it is not a Fastjson endpoint and says nothing about Internet reachability. Production verification should use harmless input consistent with the application's schema and a correlation identifier to show that the intended replica and parse lane received it. A proxy-generated error page, a cache hit, or an HTTP status code alone does not prove that Fastjson parsed the body. Direct pod ports, internal hostnames, old routes, alternate listeners, and message-driven paths must be tested against the same trust model because they may bypass the public policy layer.

Egress tracing starts from that exact worker, not from an architectural promise that the cluster “has no Internet.” Check DNS, literal-address routing, JVM proxy properties, environment and application proxies, transparent gateways, sidecars, service-mesh policies, internal object stores, artifact services, and east-west access. An attacker-controlled tenant resource, a compromised internal service, or a broadly permitted proxy can provide retrieval capability without public Internet access. Conversely, a deny-by-default policy enforced for every relevant pod or host, with telemetry showing both direct and proxy paths rejected, can interrupt the external retrieval stage of the reproduced chain. It does not remove local-classpath AutoType risk, prove the parser sound, or close unrelated deserialization routes.

Denied network activity remains evidence. A blocked DNS query or connection near a parse attempt can show that an input reached the resource-probe stage and that the egress control did useful work. Preserve the request, correlation context, destination classification, policy decision, process identity, and timestamps rather than dismissing the event because no bytes returned. A successful retrieval proves more—the resource stage was reachable—but still does not prove definition, initialization, or code execution. Those later transitions require loader and JVM evidence. This staged interpretation prevents both overstatement from a single network alert and understatement when a late parser error hides an earlier fetch.

The JDK belongs on both maps. SOSEC completed definition and marker initialization only on Temurin OpenJDK 1.8.0_492-b09 and has not completed the corresponding later-JDK matrix. Moving off JDK 8 is necessary platform maintenance, but it is not a Fastjson repair receipt. After the upgrade, ask three concrete questions: did the resource request disappear, was an unfamiliar class still defined, and can external metadata still influence type selection? Only the observed stopping point can say what that runtime changed in this deployment.

Privilege determines impact after initialization, not whether the parser crossed the execution boundary. Record the container user, filesystem mounts, writable paths, Linux or Windows privileges, token and credential sources, cloud workload identity, service-account permissions, reachable databases and control planes, and lateral network access. A restricted worker can materially reduce blast radius, yet attacker-selected class initialization remains an RCE-equivalent violation under the reproduced conditions. Assessment language should distinguish “the route can initialize supplied code” from “that code can administer the cluster,” and incident triage should measure actual privileges rather than inheriting the root shell shown in an unrelated public video.

6.3 Without a public payload fingerprint, follow what happens before and after the parse

No complete, auditable production input is public. Detection should therefore not depend on a guessed literal string. At ingress, useful signals include unusual latency or repeated failures on parse routes, unexpected type metadata, safeMode not support autoType, autoType is not support, ClassCastException, and class-name validation errors. Associate each event with request or message ID, instance, worker thread, parser configuration, and deployment digest. These signals establish an opportunity or a rejection, not exploitation. Resource-probe exceptions may be swallowed, and a successful path may be quiet, so the absence of a Fastjson stack trace is not negative evidence.

Network analytics should look for a Java workload contacting a first-seen domain or address during a parse window, HTTP or archive-like retrieval inconsistent with the service's business role, short repeated connection sequences, direct-address access, abnormal proxy requests, and unexpected east-west sessions. Baseline at the workload and lane level: an API client that routinely calls a fixed partner differs from a parser worker that has never initiated outbound traffic. Do not turn one destination, laboratory path, or marker name into a universal IOC. Fleet detection should classify parser-adjacent fetch behavior instead of depending on a single literal sample.

Class and host telemetry raise confidence by showing the next transitions. Correlate class-load or JFR events, definitions attributed to a Boot launcher from an abnormal source, newly created JARs or other files, unexpected Java child processes, credential reads, cloud-role use, control-plane calls, changes to scheduled work, and restarts not explained by deployment. A class-load event alone may reflect benign framework behavior, and a file write alone is nonspecific. The evidentiary strength comes from order and shared identity: untrusted data reaches a Fastjson lane; the same process performs an unusual resource lookup; a compatible loader defines or initializes a class; host or identity activity follows; and the parser may only then return an error.

Correlation has to survive layers. Join ingress, application, JFR or APM, EDR or eBPF, DNS, proxy, flow, container-runtime, identity, and control-plane records by pod or host, cgroup, PID, thread where available, request or message ID, and a bounded time window. Record clock skew and retention. If a queue consumer has no correlation ID, add one prospectively and use process-and-time joins for the historical hunt with an explicit confidence limit. “No exploitation found” is defensible only alongside a coverage statement naming which entries were logged, which JVMs emitted class-load data, which hosts had endpoint telemetry, and how long DNS and proxy records were retained. A visibility gap is unknown, not clean.

6.4 A negative verdict is credible only when it names the step that stopped

Valid negative evidence maps to a necessary condition. “Fastjson 1.x is not loaded” requires code-source and class-loading evidence from the process handling the target data, including old shaded, repackaged, and plugin copies. “Untrusted input is unreachable” must cover proxy, direct, message, import, batch, and retry paths. “SafeMode blocks the route” must identify the live ParserConfig, the handler inventory, and a pre-lookup rejection on the real lane. “The loader condition is absent” must be observed on the parsing worker, not the startup thread. “No egress exists” must cover DNS, direct addressing, proxies, and east-west routes, with enforcement telemetry. Bind every finding to image digest, replica set, configuration revision, and time because a rolling release, plugin change, or rebuilt executor can invalidate it.

Production state Required evidence Operational meaning
Confirmed full exposureRuntime evidence closes the untrusted entry, affected Fastjson path, compatible resource and class loaders, Temurin 8u492 or deployment-specific evidence proving definition succeeds, available egress, and absence of an effective pre-lookup SafeMode decision.Interrupt ingress or retrieval immediately and migrate at highest priority. Exposure without an observed consequence is not proof of intrusion.
Possible exposureFastjson 1.x, fat-JAR packaging, and untrusted parsing are present, but loader, TCCL, JDK, handler, or network evidence is incomplete.Missing facts are not safety facts. Time-box collection, restrict privileges and egress, and retain the asset in the high-priority queue.
Resource probe observedA parser-correlated DNS or HTTP request occurred, but evidence does not show class definition or initialization.Preserve input and network evidence as an SSRF-like retrieval event, validate containment, and continue the class-load and host hunt.
Suspected or confirmed incidentUnexpected class initialization, process, file, credential, role, or control-plane behavior is temporally and operationally connected to the parse window.Enter incident response immediately; do not wait for a public payload, CVE assignment, or shell-style callback.
Specific chain interruptedThe real lane proves that 1.x is absent, SafeMode rejects before lookup without a class-returning handler, or enforced policy blocks every direct and proxied retrieval path; a post-fetch negative also proves that neither class definition nor initialization occurred.Close only the stated mechanism condition. Keep Fastjson 1.x lifecycle removal and regression verification open where the dependency remains.

The minimum evidence packet for each verdict should contain the service and owner; entry and data producers; running Fastjson artifact and digest; complete JDK identity; packaging and loader family; parse API and expected type; ParserConfig identity; SafeMode and handlers; explicit default loader and parsing-thread TCCL; ingress and egress policy; process privilege; harmless verification outcome where authorized; telemetry coverage; incident-hunt interval; and the next review date. Unknown fields should remain visibly unknown with an owner and deadline. Substituting “probably default” prevents both SOC correlation and migration acceptance because nobody can later show which old replica actually left traffic.

7 Contain the fire first, then move archived Fastjson 1.x out of the building

Two jobs begin at once. Today, prevent incoming JSON from choosing a Java class, prevent the worker from retrieving arbitrary resources, and remove credentials and host authority the parser does not need. Then remove the archived Fastjson 1.x implementation from every untrusted lane. The first job reduces opportunity quickly; the second ends the long-term wager on a read-only dependency line. A migration plan does not contain today's exposure. A temporary switch does not prevent an old replica, handler, or configuration drift from opening the path again.

7.1 SafeMode stopped the local chain—provided no handler had already admitted the type

If Fastjson 1.x cannot be removed immediately and the deployed release is 1.2.68 or later, set -Dfastjson.parser.safeMode=true at JVM startup and restart every instance that handles the affected inputs. Earlier releases do not provide the same built-in gate; they need their entrances drained or removed, migration accelerated, type metadata stopped before Fastjson, loader reach narrowed, and egress denied while replacement proceeds. Do not put a SafeMode task against a version that does not contain the switch.

An application can also call ParserConfig.getGlobalInstance().setSafeMode(true) before its first parse, but programmatic configuration is easier to miss through initialization order, a separately constructed ParserConfig, an unrestarted process, or a framework route using another instance. Verification must name the actual configuration and feature state on each parsing lane; a deployment manifest or one startup message is not enough. In SOSEC's typed Boot-loader control, built-in SafeMode with no custom handler produced neither a resource request nor a marker. That proves it blocked the tested route before the probe on 1.2.83. It does not give earlier 1.x releases a switch they never shipped.

The qualification comes directly from call order. In the commit-pinned ParserConfig.checkAutoType(), lines 1316–1331, registered AutoTypeCheckHandler implementations are consulted before the built-in SafeMode rejection. A handler that returns a class has already made the application's type-trust decision; SafeMode does not overrule it afterward. That is an application extension point doing what it was configured to do, not an unexplained bypass of the built-in gate. Responders must search global registrations, annotation-level handlers, framework integrations, plugins, and every custom parser configuration. Remove handlers with no current business owner. A handler that must remain should map only a finite, reviewed business vocabulary to application-owned types, reject everything else, and never treat an externally supplied Java class name, package prefix, or loader-resolved resource as authorization.

Handler review needs negative cases as well as a tidy allow list on paper. Test confusingly similar package prefixes, unexpected subclasses, types visible through a different loader, values in nested object positions, and classes incompatible with the caller's declared type. Confirm that a rejection occurs before any resource lookup or class definition. Also examine deserializer-specific hooks and framework adapters that may perform their own type mapping before the global parser policy. The relevant question is not simply whether application code contains a method named addAutoTypeCheckHandler; it is whether any extension on the live route can convert untrusted metadata into a Class before the library's built-in refusal owns the decision.

A warm hand-drawn decision diagram shows the built-in SafeMode path passing through successive gates to a locked state, while an earlier application handler decision follows a red branch that requires separate audit and restriction.
Figure 3. SafeMode protects the built-in path when no earlier handler returns a type. A custom handler retains application-owned type-selection authority and must be inventoried, narrowed, and tested separately.

Verification should exercise the application's real generic parsing, typed DTO routes, message consumers, scheduled jobs, and asynchronous workers in isolated marker-only regression tests. A passing containment result has three observations: the controlled resource service receives no request, the inert marker remains absent, and telemetry identifies the SafeMode rejection on the intended parser instance. Run ordinary business fixtures alongside the negative cases so an emergency setting does not silently discard legitimate traffic. If one lane still performs a resource lookup, first determine whether it loaded another Fastjson copy, constructed another configuration, retained a custom handler, or escaped the restart. An HTTP 500 alone is not acceptance evidence; earlier chapters established that a caller-visible parse or cast failure can occur after the security-relevant transition.

7.2 Take away JSON's power to choose Java classes before Fastjson sees it

Where the business protocol does not require polymorphic type metadata, an independent component should reject special type keys and undefined fields before Fastjson can construct an arbitrary object graph. The component must parse only enough syntax to enforce a closed schema without delegating preliminary object materialization back to the vulnerable library. Fastjson's default type key can be changed at runtime, so the control must be based on the effective application configuration, an approved field set, and the protocol's finite discriminator vocabulary—not a search for one familiar literal. Where polymorphism is genuinely required, map an external discriminator to a server-maintained enum of business variants. The external value must never become a Java class name, package prefix, or class-resource selector.

Concrete DTOs, field allow lists, maximum depth, collection limits, and strict number and string bounds remain worthwhile. They reduce ambiguity, memory pressure, and the number of application types that legitimate data can reach. They do not carry the entire remediation burden: the local typed-root control showed that initialization can precede the final incompatibility error. Every schema claim must be tested at root and nested positions and across wrappers that transform, decompress, normalize, or re-encode the request before parsing. The security invariant is that unapproved type metadata is removed or refused before Fastjson resolves it, not merely that the eventual Java value fails validation.

Retire unused import, preview, webhook, diagnostic, generic-conversion, and legacy-consumer routes at the router or subscription boundary rather than parsing first and returning an error later. For lanes that remain, constrain caller and tenant identity, content type, compressed and expanded size, nesting depth, concurrency, timeout, and queue capacity. Apply the same policy to internal hostnames, direct container ports, disaster-recovery deployments, and non-HTTP consumers. A WAF can temporarily enforce route, rate, size, and known metadata rules, but it cannot reliably cover custom type keys, encoding and compression differences, disagreement between gateway and backend normalization, or messages that never traverse HTTP. With no complete public input available, a vendor rule's apparent hit rate is especially unsuitable as permanent-repair evidence.

Outbound access should be deny-by-default for the parsing workload and should cover DNS, literal-address connections, JVM-configured proxies, transparent gateways, service-mesh egress, and raw sockets. "No public Internet" is too weak if the worker can still reach tenant-controlled storage, metadata services, deployment control planes, or broad internal proxies. Allow each required destination and protocol from a documented business need, place HTTP access behind an authenticated and logged proxy where practical, and alert on rejected as well as successful parser-adjacent retrieval attempts. A denial can prove that one network transition was blocked while also preserving evidence that suspicious input reached the loader boundary.

Process isolation limits consequences if another control fails. Run the parser as a non-root identity with a read-only root filesystem, a dedicated temporary directory, minimal Linux capabilities or equivalent platform rights, and no container-management socket or writable shared deployment volume. Remove cloud credentials, signing keys, deployment tokens, and broad service identities that the parsing task does not need. Separate parsing from sensitive control-plane clients when architecture permits, and cap process, file, memory, and network resources. These measures reduce the opportunity for the reproduced chain and the impact of any successful initialization; they do not remove Fastjson's trust-before-compatibility branch. Their acceptance records must therefore remain labelled as containment, with an expiry tied to migration.

7.3 Migration is not a coordinate edit: test Fastjson2 and compatibility layers separately

The Fastjson 1.x repository is archived and read-only. The public-source review completed on 20 July 2026 found no maintainer fix commit or new 1.x release for this mechanism available for acceptance. Permanent remediation is therefore not a search for an unverified safe point within the 1.x line. It is the removal of the archived parser from untrusted processing, either by migrating to a maintained Fastjson2 API or by adopting another supported parser with explicit schema and polymorphism controls. Firsov recommends migration to Fastjson2, but SOSEC has not completed the same mechanism matrix against Fastjson2's native API and 1.x compatibility coordinates. SOSEC therefore treats Fastjson2 as a migration direction requiring acceptance, not a fixed floor already signed by local evidence.

Test native com.alibaba.fastjson2 APIs separately from compatibility artifacts and adapters. A compatibility surface may preserve type features, annotations, defaults, or extension behavior specifically to ease migration; it cannot inherit a security verdict from a native API with different configuration. Record the exact replacement artifact and digest, API overload, reader or writer features, mix-ins, modules, custom readers, handler equivalents, loader arrangement, JDK build, and framework integration. If the application enables a feature such as SupportAutoType, its result is a distinct matrix row rather than a footnote to a test performed with defaults. The same discipline applies to Jackson, Gson, or another alternative: a maintained name is not a substitute for proving that external discriminators cannot select arbitrary runtime classes.

If a maintainer patch appears, acceptance must examine more than whether lines 1505–1510 lost an early return. The source walk yields four properties that a repair has to restore:

  1. An unbounded type string must not trigger external class-resource retrieval before its namespace and source are authorized.
  2. @JSONType found in candidate bytes cannot serve as the credential that authorizes those same bytes.
  3. Every class considered for return must pass the same dangerous-parent, expected-class, and configuration policy before deserialization can use it.
  4. The bytes inspected and the class eventually loaded need a verifiable provenance relationship. One loader must not inspect resource A while another defines a different class B under the same name without policy detecting the substitution.

Those requirements leave room for several implementation designs, and no upstream patch can be presumed in advance. A maintainer might restrict annotation inspection to application-owned resources that have already passed policy, remove annotation-based load authorization entirely, or restructure type selection so compatibility and source checks dominate every return. Whatever design is chosen, regression tests should cover the generic route, a typed DTO, an incompatible expected class, explicit default loaders, mismatched and matching TCCLs, SafeMode, pre-SafeMode handlers, cached mappings, legitimate annotated models, and resource failures. The test should prove the absence of retrieval, definition, and initialization for rejected input while preserving intended business behavior. A patch commit, released artifact, and these positive and negative results together can establish a fixed version; a diff that merely moves the visible return cannot.

The regression test should make the order of failure observable, not merely expect an exception. Give the resource loader a counter, record the class returned by each defining loader, start with an absent marker, and assert all three before accepting a rejected case. A two-loader row places harmless, different implementations of the same binary name behind parent and child loaders; if the annotation is read from one resource while the other loader supplies the class, the row fails even though the name matches.

Run each row once in a fresh JVM and again after legitimate annotated models have populated Fastjson and JVM caches. This catches policy that still runs only after external lookup, provenance checked against one byte stream while another loader defines the class, and a cold-path rejection that a warm mapping silently bypasses.

Application-owned annotated models from an approved local artifact remain the positive control and should deserialize when they satisfy the expected type and configuration policy. Retain the lookup count, defining loader, CodeSource or recovered byte digest, marker state, and final exception or value for every row.

Migration discovery must extend beyond the main build file. Search executable fat JARs, shaded namespaces, plugin directories, application-server shared libraries, old task bundles, disaster-recovery images, and independently deployed consumers. At runtime, prove which bytes define com.alibaba.fastjson.JSON, ParserConfig, and any compatibility facade on each untrusted lane. Remove transitive and dormant copies or ensure they are unreachable by construction and covered by a regression test. A new Maven coordinate beside an older embedded JAR can make a dependency scanner green while the actual worker continues to resolve 1.x through a parent or plugin loader.

Stored and queued data make migration a two-sided protocol change. Inventory payloads written by 1.x that may be read after cutover, including caches, delayed messages, retry and dead-letter queues, snapshots, session state, and signed or hashed JSON. Determine whether the replacement interprets legacy type hints, dates, numeric forms, nulls, duplicate keys, and custom annotations differently, and decide whether to transform data offline, support a bounded legacy reader, or expire it. A legacy reader retained for old records must never become a general untrusted fallback: bind it to a closed store and schema, keep SafeMode and egress restrictions around it, meter every use, and assign a deletion date. The migration is not finished while arbitrary new data can still be routed to that compatibility lane.

Move supported-JDK work forward in parallel, but state the reason accurately. A service that still exposes an old parser to untrusted data on JDK 8 already needs platform maintenance; SOSEC has not completed a matrix proving that JDK 11, 17, or 21 blocks the route. Even if a future control shows class definition failing, resource retrieval may still have happened. The JDK upgrade improves the platform baseline; SafeMode closes the tested parser branch; network and process controls limit opportunity and consequence; parser migration removes the vulnerable dependency. Each has a separate acceptance result.

7.4 Before switching traffic, answer three questions: meaning, safety, and rollback

The first acceptance suite is semantic. Build a privacy-scrubbed corpus representing every live protocol and stored-data version, then add deliberate edge cases for null handling, missing and unknown fields, duplicate fields, oversized and high-precision numbers, time zones and date boundaries, generic collections, enums, inheritance, naming policy, and custom serialization. Feed the current and candidate paths the same corpus. Compare object values, business decisions, serialized output, persistence representation, cache keys, signature inputs, error categories, and retry behavior—not merely whether both calls avoid an exception. If field order has no business meaning, compare objects; if signatures or downstream protocols depend on exact bytes, compare bytes. A business owner must accept every intentional difference explicitly.

The second suite is security acceptance. Test the native replacement, each compatibility coordinate, and the actual framework integration as separate subjects. Use inert rejection fixtures to confirm that special type metadata, unapproved discriminators, excessive depth or collection size, and unsupported types stop before resource retrieval, unexpected class definition, marker creation, filesystem or process effects, or unexplained egress. Preserve parser, loader, network, and host observations so a final exception is not mistaken for an early refusal. Results from a native API cannot be assigned to a compatibility layer, and results from defaults cannot be assigned to an application that enables type support or installs custom readers.

The third suite covers rollout and recovery. Progress from offline replay to side-effect-free shadow reads, a restricted-caller canary, one worker pool, and then staged traffic expansion. At every step compare acceptance and rejection rates, latency, allocation and memory pressure, queue backlog, outbound connections, class loading, and host telemetry. A shadow path must not repeat database writes, callbacks, billing, notifications, or retries. Prove before rollout that old nodes can read data written by the candidate if rollback is part of the design. A rollback must not re-enable removed routes, broaden handlers, disable SafeMode, restore egress, or return powerful credentials to the parser. Final closure requires every running untrusted path, including old jobs, plugins, and disaster-recovery copies, to stop defining or invoking Fastjson 1.x.

Attach an evidence window to every result. Record deployment and image digests, parser artifacts, JDK and loader identities, configuration hashes, test times, participating replicas, expected telemetry, and the owner who reviewed differences. Repeat focused acceptance when a framework upgrade changes launchers or thread factories, when a plugin introduces a reader or handler, when network policy or proxy routing changes, or when a new protocol version adds polymorphism. These changes invalidate claims whose premises have changed; a result from one native API, worker TCCL, or egress policy must not silently become a fleet-wide assurance.

QuestionPassing resultResult that blocks cutover
Are semantics preserved?Object values, business decisions, persistence, cache keys, signatures, errors, and retry behavior either match or have an explicitly approved difference.Unapproved changes to data, signatures, cache behavior, stored records, error handling, or retry semantics.
Does rejection happen early enough?Native APIs, compatibility layers, and framework integrations all reject unapproved type metadata before retrieval, definition, initialization, or any file, process, or network side effect.Any unexpected request, class definition, marker, file or process effect, or unexplained egress—even when the final parse throws.
Can rollout and rollback stay controlled?Shadow reads are side-effect-free, canary metrics remain healthy, old data stays readable, and rollback does not restore old entrances, handlers, egress, or Fastjson 1.x.A rollback that reopens the route, or any old job, plugin, consumer, or disaster-recovery copy that still serves untrusted data through 1.x.

Each temporary control closes only the step it was tested against. SafeMode covers the built-in handler-free path; egress denial covers the routes actually enforced; process isolation reduces what initialization can reach; a newer JDK changes only the behavior measured on that exact build. None means the source flaw has disappeared. The permanent repair is complete only when every untrusted lane uses the accepted replacement, old 1.x copies are gone, any Fastjson2 compatibility replacement has passed its separate matrix, business behavior remains correct, rejection happens before side effects, and rollback no longer depends on restoring the archived parser.

8 The local path is closed; four unknowns still bound the conclusion

The local marker changes one critical answer: under the tested Fastjson, Boot loader, and Temurin 8u492 conditions, externally supplied class bytes can reach JVM initialization. It does not fill in the candidate class, loader route, or initialization point omitted from the original demonstration, and it supplies no victim or exploitation telemetry. The public registry and maintainer-source review completed on 20 July 2026 found no new CVE or GHSA, maintainer repair commit, or fixed 1.x release mapped to this 2026 mechanism. Stating those absences does not weaken the closed local path; it prevents that path from expanding into facts the evidence has not earned.

Evidence sourceStrongest conclusion it supportsWhat it still does not establishOperational use
Firsov's posts and approximately 8.6-second videoA named researcher claims gadget-independent RCE in Fastjson 1.2.83; the video shows a local endpoint followed by a privileged shell result.No request, endpoint source, JAR digest, loader identity, complete JDK build, or causal trace was published, so the result cannot be assigned to SOSEC's mechanism or generalized to production.Trigger urgent investigation and record the researcher's version, typed-DTO, SafeMode, and Fastjson2 statements as propositions requiring independent verification.
Pinned Fastjson 1.2.83 upstream sourceThe annotation resource probe, class-loading early return, and later compatibility checks can be inspected line by line in the same checkAutoType() revision.Source structure alone cannot prove that production has a retrievable loader, accepting JDK name semantics, or a reachable parsing entry.Locate the root cause, define repair properties, and place telemetry and acceptance at the branch that changes state.
Maven Central artifact and runtime identityThe JAR digest, CodeSource, JDK build, and loader/TCCL bind the source explanation to the bytes that actually ran.One identity does not cover shaded copies, plugins, other images, asynchronous lanes, or rollback environments.Drive inventory and prevent a build-file upgrade from masquerading as closure while old bytes remain loaded.
SOSEC local marker positive and negative matrixPinned artifacts, Temurin 8u492, and the tested Boot/JDK URL conditions close external retrieval, class definition, and initialization; SafeMode, plain-loader, typed-DTO, and TCCL controls move the stopping point.The command-free sampled results cannot attribute Firsov's demonstration, fill every intervening release, or sign for JDK 9+, Fastjson2, another framework loader, or exploitation in the wild.Turn the claim into a conditional mechanism, give every temporary control an observable pass condition, and drive inventory, detection, and repair acceptance.
Production causal telemetryType selection, unusual retrieval, new class definition, and host effects on one request and process can establish real exposure or an incident.A quiet finite window cannot prove that exploitation never occurred, and one network event cannot independently prove code execution.Set incident severity, isolation scope, evidence preservation, and the point at which possible SSRF becomes an execution case.

8.1 If production shows retrieval or an unfamiliar class, do not wait for an identifier

Response should first stop continuing harmful activity, then preserve the volatile facts that can explain it. Isolate inbound and outbound access for the affected workload; retain the image digest, hashes of the loaded Fastjson artifact and enclosing fat JAR, JVM arguments, full runtime build, loader and thread identities, process tree, open connections, proxy and DNS records, container events, relevant request metadata, and the business identity that originated or processed the data. Whether to terminate before collection depends on what the workload is doing now. Active execution or external communication makes containment dominant; a quiet workload under controlled forensic conditions may justify acquiring volatile evidence first. Record the choice, time, and decision owner in the case so the later absence of process state is not mistaken for absence of activity.

Keep reviewable incident facts separate from material that would reproduce the attack. Exact hostile requests, retrieved objects, and suspicious class bytes belong only in the restricted incident repository, with collection time, originating sensor, cryptographic digest, access log, and an unmodified original beside every derived copy. Ordinary tickets and broad chat channels should carry a case identifier, hashes, a sanitized event sequence, and the earliest proven state change—not reusable inputs. Normalize timestamps to UTC while retaining the source timezone and clock-skew estimate, and preserve request, pod, PID, thread, and message identifiers before systems are rebuilt. This chain of custody lets reviewers distinguish production evidence from a laboratory canary and connect a late parser exception to earlier network or loader activity.

After rebuilding from a trusted image, rotate every credential and token the workload could reach, inspect neighboring services and older replicas, and determine whether queued or retained messages can replay the same path. The hunt window should extend backward from disclosure to the first deployment of the exposed parsing lane or the last known-safe change, not begin automatically on 19 July. Correlate parser opportunities with unfamiliar DNS or HTTP retrieval, class-loading events, loader provenance, and host or control-plane effects. A denied outbound attempt is evidence that one control probably worked and that the triggering request deserves preservation; a successful fetch is evidence of resource reachability, not by itself proof of class initialization.

A finding of no incident indicators needs an auditable coverage statement. Record which entrances retain request or message metadata, which JVMs expose class-load or APM events, which hosts have EDR or eBPF visibility, how long DNS and proxy logs persist, and which asynchronous consumers lack a correlation identifier. A visibility gap must be written as a gap, never converted into “no exploitation found.” The laboratory's PWNED2 file is likewise not a production indicator and must not be added to scanner rules. Useful detection describes a causal neighborhood: a parsing opportunity followed by unfamiliar resource retrieval, abnormal loader behavior, and a host-side consequence on the same workload and within a defensible time window.

8.2 When can this investigation actually be closed?

Closure is not a generic four-hour or seventy-two-hour template; it is an answer about the live system. “Absent” requires runtime evidence that the process serving the lane does not load Fastjson 1.x, including old shaded, repackaged, and plugin copies. “Present, route interrupted” must name the first stopped step: an entry removed before parsing, a live SafeMode instance where no earlier handler returns a class and the built-in gate rejects before resource retrieval, or an enforced retrieval policy covering every relevant replica. “Present and exposed” keeps containment and migration open. “Unresolved” remains visible until an owner produces the missing runtime fact. A green dependency scan, an HTTP 500, or a JVM flag in source control cannot stand in for those observations.

An internal or customer-facing update should follow the same factual line. Name which products actually load 1.x, which entrances accept externally influenced JSON, where the route was observed to stop, whether retrieval, class loading, or host anomalies appeared, which telemetry interval was searched, what containment is live, and when the next update will be issued. If a point is still being tested, say “under verification.” Do not borrow CVE-2022-25845, which belongs to an older Fastjson issue, to fill the identifier gap. Do not turn “no anomaly in the available window” into “never exploited.”

8.3 From “fixed” in 2022 to a newly opened question in 2026

Fastjson 1.2.83 was released as a security update in the final 1.x line. It addressed previously known 2022 AutoType issues; CVE-2022-25845 must not be reused as the identifier for the mechanism reported here.

The upstream 1.x repository became read-only, making migration part of lifecycle disposition. Archival is not evidence that this mechanism was repaired.

Firsov published a local Fastjson 1.2.83 demonstration, followed by claims covering 1.2.68–1.2.83, no classpath-gadget prerequisite, SafeMode mitigation, and Fastjson2 non-exposure. The public material omitted the complete input, server program, and causal trace.

Using official Fastjson 1.2.83, Temurin 8u492, and two Spring Boot loader generations, SOSEC's inert canary connected external retrieval, annotation recognition, class definition, and initialization. The first version matrix was then discarded after parent-classpath contamination was found; a clean rerun fixed the typed DTO, TCCL, SafeMode, AutoType, and ten discrete version results.

Four gaps remain explicit. SOSEC has not determined on JDK 9, 11, 17, 21, or other vendor builds whether the route stops at retrieval, class definition, or an earlier point; until those deployments are tested, an upgrade cannot be credited with reducing the class-definition condition, and SSRF defenses remain necessary. Native Fastjson2 APIs and the 1.x compatibility coordinate have not undergone the equivalent matrix, so “Fastjson2 is unaffected” remains attributed rather than locally established.

The sampled Fastjson releases do not constitute an every-version run, and there is no maintainer-confirmed repair commit or fixed 1.x release. Firsov's original demonstration and SOSEC's local mechanism still lack one reviewable common trace, while exploitation in the wild lacks a victim, sample, time window, and detection method. These gaps do not postpone containment; they bound subsequent regression tests and production conclusions.

8.4 Start with these primary materials if you continue the investigation

Ongoing monitoring should prioritize original researcher posts, maintainer-controlled Fastjson and Fastjson2 repositories, formal vulnerability databases, and incident reports backed by reviewable telemetry. Proof-of-concept material can propose a testable claim, but it cannot replace pinned source, artifact digests, or independent controls. Maintainer confirmation or denial, a reconstructable experiment, a reasoned patch commit, a released artifact, a matching new CVE or GHSA, credible exploitation evidence, or a researcher's correction changes only the mechanism, scope, repair, or threat conclusion it directly supports. Additional reposts, view counts, and unattributed scanning rules do not change the evidence grade.

Kirill Firsov's initial Fastjson 1.2.83 claim and demonstration; the follow-up version-range and no-classpath-gadget statement; the SafeMode and Fastjson2 migration follow-up; the typed-DTO follow-up.

Chaitin Emergency Response Center reproduction notice; Qi-Anxin CERT QVD-2026-43021 notice index; Hunter Security Lab notice index; Tencent Cloud announcement 2381, “Fastjson remote code execution risk”.

Fastjson 1.2.83 source tree at commit 26f13f84; Maven Central Fastjson 1.2.83 artifact directory; pinned Fastjson 1.x SafeMode documentation; pinned Fastjson 1.x-to-Fastjson2 migration guide.

The investigation stops at a defensible place. Pinned source explains why trust arrives before compatibility. A harmless local experiment carries external class bytes through initialization on the tested Temurin 8u492 path, and a suspiciously uniform first matrix was found to be contaminated, discarded, and rerun. What remains is equally specific: maintainer confirmation and repair, controlled later-JDK and Fastjson2 results, a common trace for Firsov's demonstration, and reviewable evidence for exploitation in the wild. Production teams do not need to wait for those blanks to be filled. Find the JVM that actually parses the data, close the earliest controllable step, and prove that old bytes have left the lane. When new primary evidence appears, SOSEC will update the corresponding conclusion along the same causal line instead of treating louder repetition as a new technical fact.

Research record

9Evidence, objects, and sources

The material below preserves the identifiers and references used in this report.

9.1Research objects

Products, actors, techniques, affected objects, and control points discussed in the report.

Standards identifier statusNo matching CVE/GHSA found in the 2026-07-20 public-database review

Do not borrow identifiers from older Fastjson vulnerabilities or present a third-party advisory number as maintainer or standards-database confirmation; this result has a dated evidence boundary.

Locally reproduced versionsFastjson 1.2.48 / 1.2.66 / 1.2.67 / 1.2.68 / 1.2.80 / 1.2.83

Discrete marker-only successes under the tested Temurin 1.8.0_492-b09 and Spring Boot loader conditions; they do not prove every intervening release is affected.

Tested runtime conditionTemurin 1.8.0_492-b09 + tested Spring Boot loader/TCCL

The reproduced execution boundary depends on the effective resource and class loader, TCCL, JDK, parser entry, and egress.

Temporary control-Dfastjson.parser.safeMode=true

Verified on the built-in 1.2.83 path before resource retrieval; custom AutoTypeCheckHandler decisions must be audited separately.

Vulnerable source branchParserConfig.checkAutoType():1479-1528 @ 26f13f84fdd522de10678e43f55fde918ab7b347

The @JSONType early return precedes dangerous-parent and expectClass compatibility checks in the pinned 1.2.83 source.

9.2Event chronology

  1. Fastjson 1.2.83 released

    Fastjson 1.2.83 was released as a security update in the final 1.x line. It addressed previously known 2022 AutoType issues; CVE-2022-25845 must not be reused as the identifier for the mechanism reported here.

  2. Fastjson 1.x repository archived

    The upstream 1.x repository became read-only, making migration part of lifecycle disposition. Archival is not evidence that this mechanism was repaired.

  3. Firsov publishes the RCE claim

    Firsov published a local Fastjson 1.2.83 demonstration, followed by claims covering 1.2.68–1.2.83, no classpath-gadget prerequisite, SafeMode mitigation, and Fastjson2 non-exposure. The public material omitted the complete input, server program, and causal trace.

  4. SOSEC completes the marker-only loader chain

    Using official Fastjson 1.2.83, Temurin 8u492, and two Spring Boot loader generations, SOSEC's inert canary connected external retrieval, annotation recognition, class definition, and initialization. The first version matrix was then discarded after parent-classpath contamination was found; a clean rerun fixed the typed DTO, TCCL, SafeMode, AutoType, and ten discrete version results.

9.3Sources and material

  1. Kirill Firsov's initial Fastjson 1.2.83 claim and demonstrationhttps://x.com/k_firsov/status/2078872293745570032
  2. the follow-up version-range and no-classpath-gadget statementhttps://x.com/k_firsov/status/2078872296476057713
  3. the SafeMode and Fastjson2 migration follow-uphttps://x.com/k_firsov/status/2078872298619314610
  4. the typed-DTO follow-uphttps://x.com/k_firsov/status/2079058061801726124
  5. Chaitin Emergency Response Center reproduction noticehttps://mp.weixin.qq.com/s/cfz3mrZdnobLEjQ9f6UWjA
  6. Qi-Anxin CERT QVD-2026-43021 notice indexhttps://weixin.sogou.com/weixin?type=2&query=%E6%97%A0%E9%9C%80%20gadget%20%E9%BB%91%E5%90%8D%E5%8D%95%E5%A4%B1%E6%95%88
  7. Hunter Security Lab notice indexhttps://weixin.sogou.com/weixin?type=2&query=%E3%80%90%E6%BC%8F%E6%B4%9E%E9%A2%84%E8%AD%A6%E3%80%91Fastjson%20%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E9%A2%84%E8%AD%A6%EF%BC%9A1.2.68%E2%80%931.2.83%20%E5%8F%97%E5%BD%B1%E5%93%8D
  8. Tencent Cloud announcement 2381, “Fastjson remote code execution risk”https://cloud.tencent.com/announce/detail/2381
  9. Fastjson 1.2.83 source tree at commit 26f13f84https://github.com/alibaba/fastjson/tree/26f13f84fdd522de10678e43f55fde918ab7b347
  10. Maven Central Fastjson 1.2.83 artifact directoryhttps://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.83/
  11. pinned Fastjson 1.x SafeMode documentationhttps://github.com/alibaba/fastjson/wiki/fastjson_safemode/72c9ccb91d0de628712583f8b8a40555dcb84748
  12. pinned Fastjson 1.x-to-Fastjson2 migration guidehttps://github.com/alibaba/fastjson2/blob/f68fd88fa63575b8b2956c8c5458be718023974d/docs/fastjson_1_upgrade_en.md#L3-L37