Security advisory
Mozilla Rhino on the classpath enables arbitrary code execution through unsafe deserialization
Disclosure summary — October 23, 2026. I reported the issue privately on June 10. Over the following months, I provided the Rhino maintainers with complete technical details, JVM and Android reproducers, affected-version matrices, candidate hardening patches, and advance notice of public disclosure. The maintainers discussed the initial report and began prototype mitigation work. After I later supplied the universal chain and complete cross-version review package, they formally accepted the report and requested a CVE. I opened a CERT/CC VINCE case expecting CERT/CC to assume coordination and supplied the requested technical materials. I continued to carry the assessment, coordination, and publication planning through disclosure. I postponed publication until October 23 to give the maintainers additional remediation time. Public disclosure now occurs 135 days after the initial report. Responsible disclosure combines reasonable remediation time and practical support with timely public notice once maintainers have received the evidence, mitigation options, and advance notice. As of publication,
GHSA-f4j5-jgvj-7xmvremains private, the CVE request remains under review, and remediation relies on application controls while maintainer patch work continues. In February 2026, I estimated that the number of potentially vulnerable clients was on the order of 800 million. I welcome independent retrospective forensic analysis that could validate this estimate and better establish the scope and duration of the exposure.This research focused primarily on Android. In practical terms, an affected Rhino library on the classpath plus a single attacker-reachable unsafe Java deserialization entry point may be sufficient for arbitrary code execution with the full privileges of the deserializing process.
Impact
Ordinary native Java deserialization is sufficient. From that boundary onward, the universal chain is self-contained and resolves exclusively JDK and Rhino classes.
Exploitation begins when attacker-controlled data reaches native Java deserialization while affected Rhino classes are resolvable. This condition applies equally to applications that invoke Rhino directly and applications that merely carry it as a dependency.
The relevant unsafe application boundary exists wherever attacker-controlled data enters native Java deserialization, independent of publication timing. Rhino turns that boundary into a self-contained execution path. Defenders can respond immediately by removing the boundary, constraining it with a strict filter, and inventorying old or shaded Rhino copies.
This follows the same class-loader exposure model made familiar by the 2015 Apache Commons Collections deserialization issue: classpath availability supplies the dependency side of the chain. If a network-reachable entry point deserializes attacker-controlled data with affected Rhino classes available, the chain can provide remote code execution in the deserializing process.
Android exposure is more direct when an exported component accepts Serializable data from unrestricted callers through an Intent or Bundle. Another application can then trigger execution inside the target application's process and permission context. If the target is privileged or runs as a system application, the caller may gain execution under a higher-trust identity and inherit the target's permissions, data access, and authenticated state.
Affected versions
All 25 Maven Central artifacts from 1.7R3 through 1.9.1 are affected, with the range ordered by release chronology.
Chain coverage is:
- GetterSlot variants:
1.7R3through1.7.13 - Universal chain:
1.7.12through1.9.1
The affected artifacts are:
1.7R3, 1.7R4, 1.7R5, 1.7.6, 1.7.7, 1.7.7.1, 1.7.7.2,
1.7.8-RC1, 1.7.8, 1.7.9, 1.7.10, 1.7.11-RC1, 1.7.11-RC2, 1.7.11,
1.7.12, 1.7.13, 1.7.14-RC1, 1.7.14, 1.7.14.1, 1.7.15, 1.7.15.1,
1.8.0, 1.8.1, 1.9.0, 1.9.1
The affected inventory also includes shaded, renamed, and SDK-bundled copies that contain org.mozilla.javascript classes under other coordinates.
How the affected range expanded
The first working variants reached JavaScript through GetterSlot. That path ended when AccessorSlot began storing its accessor fields as transient data in 1.7.14-RC1, which initially made the newer line look hardened against the reported technique.
The underlying behavior was broader than that one accessor path. Rhino still performed function-capable coercion during equality and comparison operations. Following those paths produced the CallFrame and TreeMap composition for 1.7.12 through 1.9.1, plus the independent Symbol.toPrimitive composition on 1.9.1.
The result was continuous coverage across all 25 releases: GetterSlot and JavaAdapter variants covered the earlier artifacts, while the universal composition preserved the deserialization-to-execution primitive across the later ones.
Root cause
Behavior-rich Rhino runtime objects implement Serializable. Their equality, comparison, coercion, default-value, and accessor paths can invoke functions. During reconstruction of a crafted graph, ordinary JDK collections call those paths before readObject() returns, eventually reaching either attacker-controlled JavaScript or a public Java method with attacker-controlled bound arguments.
Universal CallFrame and TreeMap chain
The universal composition is:
ObjectInputStream.readObject()
HashMap.readObject()
HashMap.putVal()
Interpreter$CallFrame.equals()
ScriptRuntime.doTopCall()
ContextFactory.doTopCall()
Interpreter$CallFrame.equalsInTopScope()
EqualObjectGraphs.equalGraphs()
EqualObjectGraphs.equalObjectArrays()
AbstractMap$SimpleEntry.equals()
AbstractMap.equals()
TreeMap.get()
StringLikeComparator.compare()
ScriptRuntime.toString()
ScriptRuntime.toPrimitive() [where present]
ScriptableObject.getDefaultValue()
BoundFunction.call()
FunctionObject.call()
MemberBox.invoke()
Method.invoke()
attacker-selected public Java method
with bound arguments
Versions 1.7.12 through 1.7.15.1 use NativeArray$StringLikeComparator. Versions 1.8.x and 1.9.x use ArrayLikeAbstractOperations$StringLikeComparator and include the distinct ScriptRuntime.toPrimitive() frame.
GetterSlot variants
The GetterSlot and PriorityQueue composition is:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDownUsingComparator()
NativeArray$StringLikeComparator.compare()
ScriptRuntime.toString()
ScriptableObject.getDefaultValue()
ScriptableObject$GetterSlot.getValue()
MemberBox.invoke()
Context.enter()
InterpretedFunction.call()
attacker-controlled Rhino JavaScript
A GetterSlot and JavaAdapter composition covers 1.7R3 through 1.7.13. AccessorSlot uses transient accessor fields beginning with 1.7.14-RC1, ending this variant's range.
Symbol.toPrimitive variant
Rhino 1.9.1 has an independent PriorityQueue composition that reaches bound Java method invocation through symbol-based primitive coercion:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDownUsingComparator()
ArrayLikeAbstractOperations$StringLikeComparator.compare()
ScriptRuntime.toString()
ScriptRuntime.toPrimitive()
ScriptableObject.getProperty(Symbol.toPrimitive)
BoundFunction.call()
FunctionObject.call()
MemberBox.invoke()
Method.invoke()
attacker-selected public Java method
with bound arguments
This variant uses a serialized proxy key equal to SymbolKey.TO_PRIMITIVE.
Runtime behavior
The chains executed during readObject() for all 25 affected artifacts on the JVM, and deserialization returned normally in each case. The universal graph resolved exclusively JDK and Rhino classes under an ObjectInputFilter configured to admit those classes. Context.getCurrentContext() returned null both before and after deserialization.
Android reachability
All 25 artifacts reached execution across separate Android applications and processes. Version-matched streams created under ART were delivered by explicit Intent to an exported activity, and execution occurred inside the target process during readObject().
The 11 universal-chain artifacts returned a HashMap. The 14 GetterSlot and JavaAdapter artifacts reached execution before java.io.IOException. The exported activity used a manifest with zero declared Android permissions, and Context.getCurrentContext() returned null after deserialization.
Coordination and fix status
The maintainers discussed the initial report and explored disabling serialization by default while retaining an explicit compatibility opt-in. A private prototype followed. I later discovered the universal CallFrame and TreeMap chain, extended the affected range through 1.9.1, and supplied complete JVM and Android reproduction matrices together with candidate hardening patches for the current branch, the 1.9 line, and 1.7.15.1.
Formal acceptance and the CVE request followed my delivery of the universal chain and complete review package. After giving advance notice, I postponed publication until October 23 to provide additional remediation time. At disclosure, 135 days after the initial report, CVE review, the private GitHub advisory workflow, and pre-release patch work remained in progress. Publication proceeded independently on October 23.
Remediation
At publication, application-level controls were the available remediation path while fixed-release work remained in progress. Upgrade every direct, transitive, shaded, renamed, or SDK-bundled copy when a fixed release becomes available.
Fixed releases should reject native Java serialization of behavior-rich runtime objects by default. Any compatibility opt-in should be restricted to authenticated, integrity-protected data from a trusted source, and application policy should take precedence over a dependency's opt-in.
Applications that retain native Java serialization should install a context-specific ObjectInputFilter before the first readObject(). The filter should allow only required graph types, reject undecided classes, and enforce limits on depth, references, array sizes, and bytes. Protection must occur before the first read because callbacks can execute ahead of post-read casts and validation.
Android applications should audit exported components and every path that accepts Serializable extras or creates an ObjectInputStream over bytes obtained from an Intent, Bundle, Parcelable, file, database, backup, or IPC channel.
Further disclosure
Further technical details and supporting material will be published on November 6, 2026, two weeks after this disclosure.
References
- Oracle serialization filtering
- Oracle ObjectInputFilter API
- Android unsafe deserialization guidance
- CERT Java serialization rules
- Apache Commons Collections COLLECTIONS-580
- Apache Commons Collections security information
- CERT/CC VU#576313
- XStream security framework
Credit
Antoni Tremblay