Skip to content

Harden ObjectArrayMessage deserialization with SerializationUtil.assertFiltered#4098

Open
SunWeb3Sec wants to merge 11 commits into
apache:2.xfrom
SunWeb3Sec:harden-message-deserialization
Open

Harden ObjectArrayMessage deserialization with SerializationUtil.assertFiltered#4098
SunWeb3Sec wants to merge 11 commits into
apache:2.xfrom
SunWeb3Sec:harden-message-deserialization

Conversation

@SunWeb3Sec
Copy link
Copy Markdown

@SunWeb3Sec SunWeb3Sec commented Apr 19, 2026

Harden ObjectArrayMessage.readObject() with SerializationUtil.assertFiltered()

Summary

Adds a single SerializationUtil.assertFiltered(in) call at the top of ObjectArrayMessage.readObject(), bringing it in line with the defensive pattern already used by ObjectMessage and ParameterizedMessage.

Change

log4j-api/.../message/ObjectArrayMessage.java

Before

private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    array = (Object[]) in.readObject();
}

After

private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
    SerializationUtil.assertFiltered(in);
    in.defaultReadObject();
    array = (Object[]) in.readObject();
}

Plus the corresponding import org.apache.logging.log4j.util.internal.SerializationUtil;.

What this PR deliberately does not do

  • Does not change the serialized wire format — ObjectArrayMessage instances produced by older versions still deserialize into a patched version, and vice versa.
  • Does not touch LocalizedMessage or FormattedMessage. They have similar gaps, but the Log4j security team scoped this request to ObjectArrayMessage only. Happy to submit follow-up PRs for those if desired.
  • Does not modify existing tests (LocalizedMessageTest#testSerialization*, FormattedMessageTest#testSerialization) that use plain ObjectInputStream, because those classes are untouched.

Tests

Added a minimal round-trip test (ObjectArrayMessageTest#testSerializableRoundTripThroughFilteredStream) that serializes and deserializes through SerialUtil, which uses FilteredObjectInputStream on Java 8 — verifying the new assertFiltered() call accepts filtered streams.

Behavioral note

On Java 8, deserializing an ObjectArrayMessage through a plain ObjectInputStream (no JEP 290 filter) now throws IllegalArgumentException instead of silently proceeding. This matches the existing behavior of ObjectMessage and ParameterizedMessage. Callers that relied on unfiltered deserialization of ObjectArrayMessage should wrap their streams in FilteredObjectInputStream, matching the project's guidance for the sibling message types.

On Java 9+, the behavior is unchanged — assertFiltered() is a no-op when a JVM-level ObjectInputFilter is active, and a warning otherwise.

Checklist

  • Single class changed (ObjectArrayMessage)
  • No wire format change
  • No new public API
  • No new dependencies
  • Unit test added
  • Changelog entry: src/changelog/.2.x.x/harden_message_deserialization.xml (type changed)

References

  • Private discussion with the Apache security team (closed as Informative — not a vulnerability; code-quality improvement welcomed).
  • Hardened siblings for reference:
    • ObjectMessage#readObject
    • ParameterizedMessage#readObject

Thanks to the Apache security team for the clear triage.

…rtFiltered

Adds a SerializationUtil.assertFiltered(in) call at the top of
ObjectArrayMessage#readObject, bringing it in line with the defensive
pattern already used by ObjectMessage and ParameterizedMessage.

This is a defense-in-depth / consistency fix; the serialized wire
format is unchanged so instances produced by older versions continue
to round-trip.

Signed-off-by: SunWeb3Sec <infosecpt@gmail.com>
@vy vy self-assigned this Apr 28, 2026
@vy vy added the enhancement Additions or updates to features label Apr 28, 2026
@vy vy added this to the 2.26.0 milestone Apr 28, 2026
@vy
Copy link
Copy Markdown
Member

vy commented Apr 28, 2026

@SunWeb3Sec, would you mind applying this DiD in all private void readObject\((final )?ObjectInputStream hits, please? You can skip tests, just do the necessary changes along with a very brief change log entry.

SunWeb3Sec and others added 2 commits April 29, 2026 07:52
…tream)

Extends the previous ObjectArrayMessage hardening to every remaining
private void readObject(ObjectInputStream) implementation across
log4j-api, log4j-core, log4j-1.2-api, log4j-slf4j-impl, log4j-slf4j2-impl,
and log4j-perf-test, per reviewer request.

Defense-in-depth only; serialized wire format is unchanged.

Signed-off-by: SunWeb3Sec <infosecpt@gmail.com>
@SunWeb3Sec
Copy link
Copy Markdown
Author

@vy Done, broadened to all private void readObject(ObjectInputStream) hits across the listed modules. Please review. thanks

Comment thread src/changelog/.2.x.x/harden_message_deserialization.xml Outdated
Copy link
Copy Markdown
Member

@vy vy left a comment

Choose a reason for hiding this comment

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

@SunWeb3Sec, thanks so much for the contribution and prompt reaction. Changes LGTM.

@vy vy enabled auto-merge (squash) April 29, 2026 09:20
Comment thread src/changelog/.2.x.x/harden_message_deserialization.xml Outdated
@vy
Copy link
Copy Markdown
Member

vy commented May 2, 2026

@SunWeb3Sec, could you please fix the test failures? Make sure ./mvnw verify passes.

@vy vy modified the milestones: 2.26.0, 2.x May 2, 2026
The PR's `assertFiltered(in)` calls reject plain `ObjectInputStream` on
Java 8. Update the affected tests to deserialize via `SerialUtil` (or
`FilteredObjectInputStream` for the JUnit 4 helper in `log4j-1.2-api`)
so they work on both Java 8 and Java 9+.
auto-merge was automatically disabled May 3, 2026 02:53

Head branch was pushed to by a user without write access

@SunWeb3Sec
Copy link
Copy Markdown
Author

Updated — routed the affected readObject tests through SerialUtil / FilteredObjectInputStream so they pass the new assertFiltered check on Java 8.

@vy vy modified the milestones: 2.x, 2.27.0 May 18, 2026
@vy vy enabled auto-merge (squash) May 18, 2026 08:58
@vy
Copy link
Copy Markdown
Member

vy commented May 18, 2026

@SunWeb3Sec, see the failing CI results. Have you made sure ./mvnw verify?

The Java 8 surefire run (`java8-tests` profile) goes through
FilteredObjectInputStream, whose default allow-list covers
`org.apache.logging.log4j.` but not the `org.apache.log4j.`
1.2-compatibility namespace. LevelTest#testDeserializeINFO and
#testCustomLevelSerialization therefore failed with
"Class is not allowed for deserialization" on Java 8.

Pass the two 1.2 classes the tests in this module deserialize
(`org.apache.log4j.Level`, `LevelTest$CustomLevel`) as the
`allowedExtraClasses` set so the hardened `Level#readObject`
check still passes.

Signed-off-by: SunWeb3Sec <infosecpt@gmail.com>
auto-merge was automatically disabled May 18, 2026 12:27

Head branch was pushed to by a user without write access

@SunWeb3Sec
Copy link
Copy Markdown
Author

Fixed — the Java 8 surefire run uses FilteredObjectInputStream, whose default allow-list covers org.apache.logging.log4j. but not the org.apache.log4j. 1.2 namespace, so LevelTest#testDeserializeINFO and #testCustomLevelSerialization failed with InvalidObjectException. Passed org.apache.log4j.Level and LevelTest$CustomLevel as allowedExtraClasses in SerializationTestHelper.

// FilteredObjectInputStream's default allow-list covers `org.apache.logging.log4j.` but not
// the `org.apache.log4j.` 1.2-compatibility namespace, so we have to enumerate the
// 1.2 classes that the tests in this module deserialize on Java 8.
private static final Collection<String> ALLOWED_LOG4J_1_2_CLASSES =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you inline this to newObjectInputStream as a local variable, please? You can do if (ver == 8) { your-var-and-code-goes-here; } else { ... }. Keep the comments above the local var.

@vy
Copy link
Copy Markdown
Member

vy commented May 18, 2026

@SunWeb3Sec, tests are still failing. Would you mind updating the PR and ensuring that ./mvnw verify passes locally before pushing your changes, please?

@vy vy added the waiting-for-user More information is needed from the user label May 18, 2026
Sun added 2 commits May 19, 2026 08:26
Move the `org.apache.log4j.` allow-list into `newObjectInputStream`
so the constant lives in the only place that uses it, per review
feedback. The explanatory comment now sits directly above the local
variable inside the `JAVA_MAJOR_VERSION == 8` branch.

No behavior change.

Signed-off-by: SunWeb3Sec <infosecpt@gmail.com>
Fixes two regressions surfaced when running `./mvnw verify` locally:

* `org.apache.logging.slf4j.Log4jLogger` lives outside the
  `org.apache.logging.log4j.` namespace covered by
  `FilteredObjectInputStream`'s default allow-list, so the Java 8 surefire
  run of `SerializeTest` rejected it. Extend `SerialUtil` and
  `SerializableMatchers` with overloads that accept an additional
  allow-list and pass `Log4jLogger` through from the test.

* Adding `import org.apache.logging.log4j.util.internal.SerializationUtil`
  to log4j-core created a cross-bundle OSGi `Import-Package` requirement
  on an unexported package, breaking `log4j-osgi-test`. Mirror the
  `core.util.internal.instant` package and ship a `package-info.java`
  that exports `util.internal` with javadoc warning external users away.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Additions or updates to features waiting-for-user More information is needed from the user

Projects

Development

Successfully merging this pull request may close these issues.

2 participants