Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions server/src/main/java/org/opensearch/bootstrap/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputFilter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -116,6 +117,27 @@ public void run() {
});
}

/**
* Installs a process-wide serial filter that rejects all Java deserialization by default.
* Plugins that legitimately require Java serialization (e.g., security plugin's user attribute caching)
* can opt in by calling {@code ObjectInputStream.setObjectInputFilter()} on their specific stream,
* which overrides the JVM-wide filter for that stream.
* <p>
* Gated behind the {@code bootstrap.serial_filter} setting (disabled by default).
*/
static void initializeSerialFilter() {
try {
ObjectInputFilter.Config.setSerialFilter(REJECT_ALL_FILTER);
} catch (IllegalStateException e) {
// Filter already set (e.g., via -Djdk.serialFilter system property or in tests)
LogManager.getLogger(Bootstrap.class).debug("Serial filter already initialized", e);
}
}

static final ObjectInputFilter REJECT_ALL_FILTER = filterInfo -> filterInfo.serialClass() == null
? ObjectInputFilter.Status.UNDECIDED
: ObjectInputFilter.Status.REJECTED;

/** initialize native resources */
public static void initializeNatives(Path tmpFile, boolean mlockAll, boolean systemCallFilter, boolean ctrlHandler) {
final Logger logger = LogManager.getLogger(Bootstrap.class);
Expand Down Expand Up @@ -183,6 +205,10 @@ static void initializeProbes() {
private void setup(boolean addShutdownHook, Environment environment) throws BootstrapException {
Settings settings = environment.settings();

if (BootstrapSettings.SERIAL_FILTER_SETTING.get(settings)) {
initializeSerialFilter();
}

try {
spawner.spawnNativeControllers(environment, true);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ private BootstrapSettings() {}
);
public static final Setting<Boolean> CTRLHANDLER_SETTING = Setting.boolSetting("bootstrap.ctrlhandler", true, Property.NodeScope);

public static final Setting<Boolean> SERIAL_FILTER_SETTING = Setting.boolSetting("bootstrap.serial_filter", false, Property.NodeScope);

}
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ public void apply(Settings value, Settings current, Settings previous) {
BootstrapSettings.MEMORY_LOCK_SETTING,
BootstrapSettings.SYSTEM_CALL_FILTER_SETTING,
BootstrapSettings.CTRLHANDLER_SETTING,
BootstrapSettings.SERIAL_FILTER_SETTING,
KeyStoreWrapper.SEED_SETTING,
IndexingMemoryController.INDEX_BUFFER_SIZE_SETTING,
IndexingMemoryController.MIN_INDEX_BUFFER_SIZE_SETTING,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.bootstrap;

import org.opensearch.common.SuppressForbidden;
import org.opensearch.test.OpenSearchTestCase;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
* Tests for the process-wide deserialization filter installed by {@link Bootstrap#initializeSerialFilter()}.
* <p>
* The filter rejects all Java deserialization by default. Plugins that need deserialization
* (e.g., security plugin) opt in by calling {@code setObjectInputFilter()} on their stream,
* which overrides the JVM-wide filter for that stream.
* <p>
* Note: String/primitive types use special serialization type codes (TC_STRING) that bypass
* ObjectInputFilter checks. These tests use ArrayList to exercise the filter on real object types.
*/
@SuppressForbidden(reason = "testing the runtime serialization filter that protects against java deserialization")
public class BootstrapSerialFilterTests extends OpenSearchTestCase {

private static final boolean FILTER_INSTALLED;

static {
// Install the JVM-wide filter. This can only be set once per JVM — if another test
// or the framework already set it, the end-to-end tests are skipped.
boolean installed = false;
try {
ObjectInputFilter.Config.setSerialFilter(Bootstrap.REJECT_ALL_FILTER);
installed = true;
} catch (IllegalStateException e) {
// Already set
}
FILTER_INSTALLED = installed;
}

// --- Unit tests for the filter logic (always run) ---

public void testRejectAllFilterRejectsClasses() {
assertEquals(ObjectInputFilter.Status.REJECTED, Bootstrap.REJECT_ALL_FILTER.checkInput(filterInfo(String.class)));
}

public void testRejectAllFilterRejectsAnyClass() {
assertEquals(ObjectInputFilter.Status.REJECTED, Bootstrap.REJECT_ALL_FILTER.checkInput(filterInfo(Runtime.class)));
}

public void testRejectAllFilterUndecidedForNullClass() {
// null serialClass = stream metadata check (depth, bytes, refs), not a class resolution
assertEquals(ObjectInputFilter.Status.UNDECIDED, Bootstrap.REJECT_ALL_FILTER.checkInput(filterInfo(null)));
}

// --- End-to-end tests showing actual runtime behavior ---

/**
* When a plugin uses ObjectInputStream without setting its own filter,
* deserialization fails with InvalidClassException at runtime.
* This is the protection against unexpected deserialization in plugins/dependencies.
*/
public void testDeserializationRejectedWithoutExplicitFilter() throws Exception {
assumeTrue("JVM-wide serial filter not installed in this JVM", FILTER_INSTALLED);

byte[] serialized = serialize(new ArrayList<>(List.of("a", "b")));
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
InvalidClassException e = expectThrows(InvalidClassException.class, ois::readObject);
assertTrue(e.getMessage().contains("REJECTED"));
}
}

/**
* When a plugin explicitly sets its own filter (like security plugin's SafeObjectInputStream),
* the stream-level filter overrides the JVM-wide reject-all, and deserialization succeeds.
*/
public void testDeserializationAllowedWithExplicitFilter() throws Exception {
assumeTrue("JVM-wide serial filter not installed in this JVM", FILTER_INSTALLED);

byte[] serialized = serialize(new ArrayList<>(List.of("a", "b")));
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
// This is what security plugin does — sets a filter on the stream
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter("maxdepth=10"));
Object result = ois.readObject();
assertEquals(List.of("a", "b"), result);
}
}

/**
* Proves the stream-level filter is actually enforced — not just bypassing all checks.
* A maxdepth=2 filter allows a shallow ArrayList but rejects a deeply nested structure.
*/
public void testStreamFilterDepthConstraintIsEnforced() throws Exception {
assumeTrue("JVM-wide serial filter not installed in this JVM", FILTER_INSTALLED);

// Create a deeply nested object: ArrayList -> ArrayList -> ArrayList (depth=3)
ArrayList<Object> deep = new ArrayList<>();
deep.add(new ArrayList<>(List.of(new ArrayList<>(List.of("nested")))));

byte[] serialized = serialize(deep);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
// maxdepth=2 should reject the depth=3 structure
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter("maxdepth=2"));
InvalidClassException e = expectThrows(InvalidClassException.class, ois::readObject);
assertTrue(e.getMessage().contains("REJECTED"));
}
}

// --- Helpers ---

private static ObjectInputFilter.FilterInfo filterInfo(Class<?> clazz) {
return new ObjectInputFilter.FilterInfo() {
public Class<?> serialClass() {
return clazz;
}

public long arrayLength() {
return -1;
}

public long depth() {
return 1;
}

public long references() {
return 1;
}

public long streamBytes() {
return 0;
}
};
}

private static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
return baos.toByteArray();
}
}
Loading