An upstream report raised an alarming possibility: a class filter around Log4j2’s Java-serialized event receiver could approve an outer event and still lose control of what was deserialized inside it. Final vendor guidance was not public when we froze this research, leaving two bad options for everyone watching: dismiss an unverified claim, or repeat the phrase “Log4j RCE” without knowing what it actually applied to.
We chose a third option. Pruva reconstructed the boundary, exercised the real Apache sample TCP receiver, and ran the same network input against vulnerable and controlled targets. The result was receiver-side command execution in two fresh JVMs. It was also much narrower than the phrase “the next Log4Shell” suggests.
This is what we can responsibly say while operational details remain under embargo.
The filter worked, then the trust boundary moved
The receiver path we tested does not parse ordinary log text. It accepts Java-serialized Log4j
events over TCP. Apache’s sample server wraps the socket in
ObjectInputStreamLogEventBridge,
which uses Log4j’s
FilteredObjectInputStream
(FOIS) before calling readObject().
FOIS is a real boundary. Our direct control sent a non-allowlisted object through the same bridge, and the receiver rejected it. Any explanation that starts with “the filter simply does nothing” is wrong.
The problem appears one transition later. A legitimate serialized Log4jLogEvent uses
LogEventProxy during serialization. In the tested Log4j build, that proxy can carry message
content in a java.rmi.MarshalledObject. The outer stream sees an allowlisted event proxy and an
allowed Java carrier. The carrier’s nested object graph is still opaque bytes at that point.
When the event proxy reconstructs the message, the carrier unpacks those bytes through another object input stream. That second stream is not the FOIS instance that approved the outer class descriptors. The original class decision is therefore not transitive across the nested deserialization boundary.
- 01Untrusted peerSends a serialized event over TCP
- 02Event receiverAccepts the network object stream
- 03FOIS boundaryChecks the outer class descriptors
- 04Allowed carrierOuter event proxy passes the filter
- 05ReconstructionOpens a separate inner object stream
- 06Inner graphReceiver classpath processes its contents
We name MarshalledObject because defenders need a concrete class to inventory and reject while
coordination continues; the field-level serialized layout and payload bytes remain withheld.
That distinction is the useful new information. The vulnerable condition is not “filter absent.” It is “filter applied to one stream, then security-sensitive deserialization resumed in another.”
What the runtime proof adds
At evidence freeze, the public record did not establish four facts that change how this issue should be evaluated:
- the outer Log4j filter was active and rejected a direct non-allowlisted object;
- the real Apache sample receiver reached the second object stream during event reconstruction;
- the network path reached command execution repeatedly in fresh receiver JVMs; and
- two source-control receivers plus a JEP 290 policy rejected the unchanged final input without a process-side marker.
Together, those observations turn a source-level concern into a bounded production-path result. They also explain why broad Log4j inventory counts are not an exposure measurement: the receiver, nested transition, classpath, reachability, and runtime filtering all matter.
We tested the product path, not a local deserialization toy
A local program that calls ObjectInputStream.readObject() on attacker-provided bytes would prove
very little about a network-facing Log4j deployment. Our acceptance contract required the actual
serialized receiver path:
TCP peer
-> TcpSocketServer
-> ObjectInputStreamLogEventBridge
-> FilteredObjectInputStream
-> Log4j event proxy reconstruction
-> nested object stream
The target used the Apache logging-log4j-samples server at a fixed source commit and official
Log4j API/Core 2.26.1 artifacts. Each attempt started a fresh receiver JVM on a randomized port.
The harness separately checked service readiness, connection acceptance, the direct filter
control, nested deserialization, and the final process-side effect.
One exact final input was generated once and replayed unchanged. It produced run-unique command
markers in two fresh vulnerable receivers with different process identities. The same bytes were
then rejected by two fresh receivers using a narrow Log4j source-level control, with no marker. A
separate run configured the JDK’s built-in JEP 290 object-input filter to reject
java.rmi.MarshalledObject; it rejected the unchanged input and produced no marker.
The full reproduction contains 29 digest-bound proof artifacts: two fresh nested-capability receivers, two fresh command-execution receivers, two fresh narrow-control receivers, and one JEP 290 mitigation receiver. We are withholding the payload, helper code, dependency recipe, and artifact link until disclosure permits publishing them.
Why this is not another Log4Shell
Log4Shell became broadly exploitable because attacker-controlled text could reach lookup behavior through ordinary logging paths in a huge number of applications. The path we verified has a very different exposure model.
An application is in the demonstrated risk envelope only when all of these conditions hold:
- it deploys a Java-serialized LogEvent receiver based on this FOIS path;
- an untrusted peer can reach that listener;
- the receiver uses a vulnerable Log4j build;
- its runtime classpath contains a usable deserialization gadget surface; and
- no effective object-input filter rejects the nested carrier or inner graph.
The tested receiver had a compatible gadget surface so we could measure the worst case for that classpath. That library is not a default Log4j dependency, and its presence in our lab must not be generalized to every Log4j application.
Ordinary calls such as logger.info(attackerText) do not enter the receiver path we tested. Merely
finding log4j-core in a software inventory does not prove reachability. Internet scanning for a
single familiar Log4j port would also be misleading because operators can bind serialized
receivers to arbitrary ports or embed similar bridge code elsewhere.
Where all five preconditions hold, the impact is serious: our proof reached command execution as the receiver process account from an unauthenticated TCP peer. We did not test or claim privilege escalation.
The controls tell us where to defend
The unchanged-input controls matter more than a successful marker. A narrow change to the tested Log4j source caused the outer filter to reject the nested carrier before event reconstruction. A JEP 290 policy separately rejected the unchanged input. Neither control produced the command-side effect.
This gives defenders useful actions before a final vendor advisory is available:
- inventory Java-serialized LogEvent listeners, including copied or adapted sample-server code;
- remove those listeners when they are not required;
- otherwise bind them to authenticated, trusted network segments rather than exposing them to untrusted peers;
- configure an application-specific JEP 290 allowlist and reject nested serialization carriers that the application does not need;
- remove unnecessary gadget-bearing libraries from receiver classpaths; and
- alert on unexpected child processes from JVMs that host serialized event receivers.
These are defense-in-depth measures, not a substitute for the vendor’s final remediation. Vendor coordination and final advisory status remained unresolved when we froze this first part. We tested Log4j 2.26.1. We are not publishing a historical affected-version matrix, assigning a severity score, naming a fixed release, or treating our narrow control as the final patch before the maintainers do.
What we are withholding, and what comes next
There is enough information here for a defender to identify the architecture and inspect exposure. There is intentionally not enough to copy a weaponized stream into a socket.
The withheld material includes the field-level serialized layout and payload bytes, the execution gadget and dependency version, payload-generation code, sender implementation, exact proof input, and the reusable reproduction script. Those artifacts remain frozen in Pruva with vulnerable, narrow-control, and runtime-mitigation evidence.
After coordinated disclosure, we plan to update this article with the assigned identifier, vendor-confirmed affected and fixed versions, the agreed remediation, and a link to the public Pruva reproduction. Until then, the most important conclusion is precise and limited: a filter that correctly constrains one object stream does not protect a second stream opened later during object reconstruction.