Skip to content

Filter Java deserialization on the RemoteObservable protocol - #878

Draft
ssatybaldiyev-ship-it wants to merge 1 commit into
masterfrom
filter-remote-observable-deserialization
Draft

ssatybaldiyev-ship-it wants to merge 1 commit into
masterfrom
filter-remote-observable-deserialization

Conversation

@ssatybaldiyev-ship-it

Copy link
Copy Markdown

Context

BatchedRxEventPipelineConfigurator, LegacyTcpPipelineConfigurator and RemoteObservable deserialize bytes from the network with ObjectInputStream.readObject() and no class filter:

  • the subscribe parameters (opcode 4), read on the server by both configurators
  • the error payload (opcode 2), read on the client by RemoteObservable.fromBytesToThrowable

A peer can send a serialized object of any class on the classpath. Java runs that class's readObject during deserialization, before the cast to the expected type, so an attacker-chosen class can execute code in the worker. The server-side read happens before any authentication.

This adds FilteredDeserializer in mantis-common. It installs a JEP 290 ObjectInputFilter that allows only the classes each payload needs:

  • Subscribe parameters: primitives, java.lang classes, and java.util Map implementations, plus a few JDK-internal helper types the JDK collection code needs (Map.Entry, CollSer, Collections$UnmodifiableCollection).
  • Errors: the same, but with java.util List implementations and any Throwable subclass.

Anything else is rejected with an InvalidClassException before that class's code runs. The three call sites now call FilteredDeserializer. The wire format is unchanged, so old and new nodes interoperate.

Behavior change: a map or exception holding a field outside those types is now rejected. That includes a ConcurrentHashMap passed as subscribe parameters, and a custom exception with a field of an unusual class. A rejected error payload makes fromBytesToThrowable throw a RuntimeException.

Not covered: the data events (opcode 1) are raw bytes decoded by the job's codec, so this does not affect them. Restricting jdk.serialFilter process-wide is a deployment setting outside this repo.

Tests

  • FilteredDeserializerTest: accepted and rejected cases, including a nested gadget class whose readObject must not run.
  • One rejection test per code path: BatchedRxEventPipelineConfiguratorTest, LegacyTcpPipelineConfiguratorTest, RemoteObservableErrorDeserializationTest.
  • The two mantis-remote-observable tests are not run by ./gradlew test, because mantis-remote-observable/build.gradle excludes that whole package (some tests there need ports). They pass when run with that exclusion lifted. Narrowing the exclusion is left for a follow-up.

Checklist

  • ./gradlew build compiles code correctly
  • Added new tests where applicable
  • ./gradlew test passes all tests
  • Extended README or added javadocs where applicable

The new tests pass locally. A full local ./gradlew build akkaTest could not be completed in my environment, because Mockito's agent self-attach is blocked there (the same tests fail on an unmodified master), so CI should confirm the rest.

🤖 Generated with Claude Code

The subscribe-parameters map (opcode 4) and the error payload (opcode 2)
were read with ObjectInputStream.readObject() and no class filter, so a
peer could make the JVM instantiate any serializable class on the
classpath. The server-side read happens before any authentication.

Add FilteredDeserializer, which installs a JEP 290 ObjectInputFilter
that allows only the classes each payload needs, and use it in
BatchedRxEventPipelineConfigurator, LegacyTcpPipelineConfigurator and
RemoteObservable. The wire format is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Test Results

836 tests  +11   825 ✅ +11   10m 42s ⏱️ +18s
169 suites + 2    11 💤 ± 0 
169 files   + 2     0 ❌ ± 0 

Results for commit 71f819e. ± Comparison against base commit dbf2ac8.

♻️ This comment has been updated with latest results.

@ssatybaldiyev-ship-it
ssatybaldiyev-ship-it marked this pull request as draft October 26, 2026 10:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant