← defend.work

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-7xmv remains 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:

The affected artifacts are:

Plain text
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:

Plain text
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:

Plain text
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:

Plain text
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

Credit

Antoni Tremblay