Skip to content

Backport the deserialization hardening series to 2.25.x - #4466

Open
ppkarwasz wants to merge 7 commits into
2.25.xfrom
fix/2.25.x/deserialization-hardening
Open

ppkarwasz wants to merge 7 commits into
2.25.xfrom
fix/2.25.x/deserialization-hardening

Conversation

@ppkarwasz

Copy link
Copy Markdown
Member

Important

This PR is part of the deserialization hardening work tracked in #4168. The Logging Services PMC does not use nor recommend Java serialization/deserialization, and our security FAQ has long documented this position. This work is submitted solely to reduce the false-positive "vulnerability" reports that keep being filed regardless of that FAQ. Its utility for end users is close to zero.

Backports the following PRs from 2.x to 2.25.x, one cherry-picked commit (-x) per PR:

Differences from 2.x

Compatibility

As on 2.x, the serialized forms of Log4jLogEvent, SortedArrayStringMap, ObjectArrayMessage and ParameterizedMessage change, and their serialVersionUIDs are changed: data serialized by 2.25.5 or earlier cannot be deserialized by this version and vice versa.

Testing

./mvnw install of log4j-api-java9, log4j-api, log4j-api-test, log4j-core-java9, log4j-core, log4j-core-test and log4j-1.2-api on JDK 17: all tests pass (including the test-serial-filter execution) and the bnd baseline checks succeed.

🤖 Generated with Claude Code

ppkarwasz and others added 6 commits November 9, 2026 17:13
* Apply the outer ObjectInputFilter to nested streams

Log4j's own deserialization code now takes a neutral stance: the nested
streams created by `SerializationUtil.readWrappedObject` get exactly the
`ObjectInputFilter` of the outer stream, nothing less and nothing more.

- copy the outer filter also when the outer stream is a
  `FilteredObjectInputStream`
- remove `DefaultObjectInputFilter`: plain `ObjectInputStream`s no
  longer get the Log4j allowlist added to their filter
- remove `SerializationUtil.assertFiltered` and all its call sites
- deprecate `FilteredObjectInputStream` and document its allowlist,
  its limits and the alternatives (`ObjectInputFilter`,
  `jdk.serialFilter`)

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add PR number to changelog entries

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Import `FilteredObjectInputStream` in `SerializationUtil`

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the changelog entry of #4098

The `assertFiltered` checks added by #4098 were never released and are
removed by this PR.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Only export `util.internal` to `log4j-core`

Since `assertFiltered` is removed, `log4j-1.2-api` and the SLF4J
bindings no longer use `SerializationUtil`.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Improve `readWrappedObject` tests

The previous test also passed before the change. The new tests check
that `readWrappedObject` does not add an allowlist of its own, and that
the filter of the outer stream is also applied if the outer stream is a
`FilteredObjectInputStream`.

Suggested by @FreeAndNil.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d2a8a11)
* Reject dynamic proxies in FilteredObjectInputStream

Dynamic proxy class descriptors are resolved through
`resolveProxyClass`, not `resolveClass`, so they bypassed the
deserialization allowlist on Java 8. The JEP 290 filter used on
Java 9 and later already rejects proxies, since their synthetic class
names never match the allowlist; this aligns the Java 8 path by
rejecting proxies outright.

Part of the hardening series from #4168.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>

* Shorten `resolveProxyClass` Javadoc

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a8578d1)
* Apply the deserialization filter in test helpers on Java 9+

The `SerialUtil` test helper returned a plain, unfiltered
`ObjectInputStream` on Java 9 and later, so the deserialization
allowlist was only ever exercised by tests running on a Java 8
toolchain. It now installs `DefaultObjectInputFilter` (reflectively,
since the class must still compile and run on Java 8), combined with an
`ObjectInputFilter.Config.createFilter` delegate built from the
caller-supplied extra allowed classes, reproducing the
`FilteredObjectInputStream` semantics on modern JDKs. When
`DefaultObjectInputFilter` is absent (it only exists in the packaged
multi-release JAR), the helper falls back to
`FilteredObjectInputStream` on any Java version.

`SerializationTestHelper` in `log4j-1.2-api` now delegates to
`SerialUtil`, so its `org.apache.log4j.*` extras are honored on all
Java versions as well.

Part of the hardening series from #4168.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>

* Always use `FilteredObjectInputStream` in `SerialUtil`

`DefaultObjectInputFilter` is being removed (#4365). Using
`FilteredObjectInputStream` on every Java version checks that the
serialized forms of Log4j stay within its documented allowlist.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test log event deserialization with `jdk.serialFilter`

Adds a Surefire execution to `log4j-core-test` that runs the log event
serialization tests with the process-wide `jdk.serialFilter` recommended
by OpenJDK, and notes in `SerialUtil` that using
`FilteredObjectInputStream` in tests is not a recommendation.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Select serialization tests by tag

- tag the tests that deserialize Log4j classes with `serialization` and
  select them by tag in the `test-serial-filter` execution
- migrate `SerializedLayoutTest` to JUnit 5
- make `SerialUtil` return a plain `ObjectInputStream` if
  `jdk.serialFilter` is set, and document the two kinds of tests it is
  used for

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b89c4d)
* Serialize the LogEvent message delegate through the filtered wrapper

`Log4jLogEvent.LogEventProxy` used `java.rmi.MarshalledObject` to
transport its `Message` delegate. `MarshalledObject.get()` deserializes
its embedded bytes on a private, unfiltered `ObjectInputStream`, so the
delegate escaped the deserialization allow-list entirely.

The message is now written with the same
`SerializationUtil.writeWrappedObject`/`readWrappedObject` mechanism
used by `ObjectMessage`, which re-applies the allow-list to the nested
stream, and `java.rmi.MarshalledObject` is removed from the allow-list.

Compatibility: log events serialized by Log4j 2.8.2-2.25.x are rejected
by newer readers, since `MarshalledObject` no longer passes the filter.
Events serialized by this version remain readable by older versions,
with the message downgraded to a `SimpleMessage`. The serialized-event
fixture in `Log4jLogEventTest` is regenerated accordingly, and the
stale, unused `serializedEvent.dat` resource is removed
(`SerializedLayoutTest` always rewrites it before reading).

Part of the hardening series from #4168.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>

* Remove `MarshalledObject` from the documented allowlist and test filter

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fall back to a serializable value for unserializable objects

`SerializationUtil.writeWrappedObject` now takes a fallback, which is
written instead of the object if it is not `Serializable` or its
serialization fails. Failed serializations are logged as warnings.

- `LogEventProxy` writes a `SimpleMessage` with the formatted message,
  as it did when the message was wrapped in a `MarshalledObject`.
- `ObjectMessage` writes its formatted message.
- `ParameterizedMessage` and `SortedArrayStringMap` write the string
  representation of the value. For `SortedArrayStringMap`, this replaces
  the `null` previously written for non-serializable values.

`UnserializableMessage` in `log4j-api-test` provides a reusable example
of a `Serializable` object that cannot be serialized.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Note the serialization format change in the changelog

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Throw on nested objects rejected during deserialization

`readWrappedObject` returned `null` for any nested object that could
not be read, including classes rejected by the filter of the stream.
The nested stream now behaves like the outer one: `IOException`s,
including filter rejections, are thrown. Only classes that are not
available on the reader, `ClassNotFoundException` and `LinkageError`,
are still replaced with `null`.

Adds tests with a nested `java.net.URI`, which is not on the allowlist,
in `ObjectMessage` and in a `Log4jLogEvent`.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test deserialization of a message with an unknown class

A message whose class is allowed by the deserialization filter, but not
available to the reader, degrades to a `SimpleMessage` with the
formatted message.

The Base64 fixtures in `Log4jLogEventTest` are folded into multiple
lines.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9cb17cf)
Use the default Java serialization for all the fields of
`SortedArrayStringMap`, except `values`, which is written as a
`byte[][]` array. Each value is serialized separately, so that values
that cannot be serialized are replaced with their string representation.

`SerializationUtil` gets the `wrapObject` and `unwrapObject` helpers,
which serialize and deserialize a single object to and from a byte
array, and the `wrapObjects` and `unwrapObjects` helpers, which do the
same for each element of an array.

The serialized form is incompatible with previous versions, so the
`serialVersionUID` is changed.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6bba23c)
…Message` (#4274)

* Serialize message arguments with the array helpers

`ObjectArrayMessage` and `ParameterizedMessage` now write their
arguments as a single `byte[][]` array, built with
`SerializationUtil.wrapObjects`, instead of a length followed by the
wrapped arguments. Each argument is serialized separately, so that
arguments that cannot be serialized are replaced with their string
representation, and the arrays are allocated by the JDK.

This also fixes the serialization of a `ParameterizedMessage` with
`null` arguments, which threw a `NullPointerException`.

The serialized forms are incompatible with previous versions, so the
`serialVersionUID`s are changed.

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>

* Update src/changelog/.2.x.x/4274_wrap_object_array_message_serialization.xml

---------

Co-authored-by: Ramanathan <ramanathan@apache.org>
(cherry picked from commit 6a4d9c2)
Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
@ramanathan1504

Copy link
Copy Markdown
Contributor

@ppkarwasz can you please check this? apache/logging-log4j-samples#436

import org.apache.logging.log4j.util.StackLocatorUtil;
import org.apache.logging.log4j.util.StringMap;
import org.apache.logging.log4j.util.Strings;
import org.apache.logging.log4j.util.internal.SerializationUtil;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

org.apache.logging.log4j.util.internal has no package-info.java on 2.25.x, so log4j-api does not export it. The log4j-core bundle then has an Import-Package that nothing provides and does not resolve in OSGi. Can we use the WrappedObjects copy from #4385, so both patch lines match?

@ramanathan1504

Copy link
Copy Markdown
Contributor

One commit per source PR with the differences listed makes this easy to compare with 2.x.
SerializeTest in log4j-slf4j-impl and log4j-slf4j2-impl fails with "Could not deserialize" because Log4jLogger is outside the allowlist. Can both tests pass Collections.singleton(Log4jLogger.class.getName()) to serializesRoundTrip, as on 2.x?

@ramanathan1504 ramanathan1504 added api Affects the public API java Pull requests that update Java code port-done No further port needed: all required ports are merged, or none was needed labels Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Affects the public API java Pull requests that update Java code port-done No further port needed: all required ports are merged, or none was needed

Projects

Development

Successfully merging this pull request may close these issues.

2 participants