CVE-2026-41862
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
Spring Statemachine is a framework for building state machine applications on top of Spring, providing support for hierarchical and regional states, transitions, and the persistence of a running machine's context to external stores such as a relational database, MongoDB, Redis, or ZooKeeper.
A high-severity vulnerability (CVE-2026-41862) has been identified in Spring Statemachine. Its Kryo-based persistence backends (JPA, MongoDB, Redis, and ZooKeeper) deserialize a persisted StateMachineContext without enabling Kryo's registration requirement, so no class allowlist is enforced on read. An attacker who can write to the persistence store can supply crafted serialized bytes that name arbitrary classes, which Kryo will instantiate during deserialization, leading to remote code execution inside the application JVM.
Per OWASP, deserialization of untrusted data occurs when an application reconstructs objects from attacker-controllable serialized input without restricting which types may be instantiated, enabling object-injection and gadget-chain attacks that can culminate in remote code execution.
This issue affects versions >=3.2.0 <=3.2.4 and >=4.0.0 <=4.0.1 of Spring Statemachine.
Details
Module Info
- Product: Spring Statemachine
- Affected packages: spring-statemachine-kryo, spring-statemachine-data-redis, spring-statemachine-zookeeper, spring-statemachine-data-jpa, spring-statemachine-data-mongodb
- Affected versions: >=3.2.0 <=3.2.4, >=4.0.0 <=4.0.1
- GitHub repository: https://github.com/spring-projects/spring-statemachine
- Published packages: https://central.sonatype.com/artifact/org.springframework.statemachine/spring-statemachine-core
- Package manager: maven
- Fixed in:
- NES for Spring Statemachine 3.2.x
- Spring Statemachine 4.0.2 (OSS)
Vulnerability Info
Spring Statemachine persists a running machine's StateMachineContext by serializing it with Kryo and writing the resulting bytes to the configured backend. Each backend constructs its Kryo instance the same way, installing custom serializers for the framework types but never enabling registration enforcement:
Kryo kryo = new Kryo();
kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer());
kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer());
kryo.addDefaultSerializer(UUID.class, new UUIDSerializer());
Because Kryo.setRegistrationRequired(true) is never called, Kryo falls back to its default behavior of resolving classes by the class name embedded in the input stream. On read, Kryo will load and instantiate whatever class the serialized bytes name. An attacker who controls the persisted context, such as the JPA row, the MongoDB document, the Redis value, or the ZooKeeper znode that stores the serialized context, can therefore craft an object graph that references gadget classes present on the application classpath. When the application loads that context (for example by restoring a state machine on startup or per request), Kryo instantiates the attacker-chosen types, which can be driven to execute arbitrary code within the application JVM.
Exploitation requires the attacker to be able to write into the persistence backend used by the state machine, consistent with the advisory's low-privilege attack vector. The same unguarded configuration is present in the Redis (RedisStateMachineContextRepository), ZooKeeper (ZookeeperStateMachinePersist), and JPA/MongoDB backends, the latter two through KryoStateMachineSerialisationService.
The remediation introduces a KryoStateMachineSerialisationDefaults helper that calls kryo.setRegistrationRequired(true) and registers an explicit allowlist of framework and JDK types. With registration required, Kryo identifies classes by a registered numeric id rather than by class name, and any type that is not on the allowlist is rejected on read. Applications that persist custom state or event types must now register those types explicitly through a new Consumer<Kryo> customizer accepted by each backend. This is a behavior change: contexts written by older releases cannot be read by the fixed version, so the persistence backend must be drained or migrated during the upgrade.
This vulnerability was introduced in 2015 with Spring Statemachine 1.0.
Mitigation
Spring Statemachine versions in the affected range are End-of-Life from a community-support standpoint, and the 3.2.x line has no publicly available open-source fix; see the Spring Statemachine support policy at https://spring.io/projects/spring-statemachine#support.
Do not attempt to self-patch the Kryo configuration; the corrected behavior depends on a consistent allowlist applied across every persistence backend together with an application-supplied registration hook, and partial fixes leave deserialization paths open.
Recommended actions:
- Upgrade to a supported, fixed version of Spring Statemachine (4.0.2 or later).
- For applications that must remain on an End-of-Life line, HeroDevs Never-Ending Support (NES) provides a drop-in patched build of Spring Statemachine that closes this vulnerability. A fix for the NES 3.2.x line is available. Learn more about HeroDevs Never-Ending Support for Spring Statemachine and request coverage at https://www.herodevs.com/support/spring-nes
Credits
- This issue was discovered internally by the Spring Statemachine team.