You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@carbondata.apache.org by ch...@apache.org on 2026/10/02 02:09:26 UTC
(carbondata) branch master updated: issue-4399 unauthenticated Java deserialization RCE on the Distributed Index Server IPC endpoint (#4400)
This is an automated email from the ASF dual-hosted git repository.
chenliang613 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/carbondata.git
The following commit(s) were added to refs/heads/master by this push:
new b653d1336b issue-4399 unauthenticated Java deserialization RCE on the Distributed Index Server IPC endpoint (#4400)
b653d1336b is described below
commit b653d1336bb25b973d7b7a650d1c7c4bb3538480
Author: Liang Chen <ch...@apache.org>
AuthorDate: Fri Oct 2 10:09:19 2026 +0800
issue-4399 unauthenticated Java deserialization RCE on the Distributed Index Server IPC endpoint (#4400)
---
.../carbondata/core/index/IndexInputFormat.java | 14 ++-
.../core/util/ObjectSerializationUtil.java | 103 +++++++++++++++++++++
.../com/attacker/EvilDeserializationPayload.java | 34 +++++++
.../util/ObjectSerializationUtilSecurityTest.java | 63 +++++++++++++
4 files changed, 211 insertions(+), 3 deletions(-)
diff --git a/core/src/main/java/org/apache/carbondata/core/index/IndexInputFormat.java b/core/src/main/java/org/apache/carbondata/core/index/IndexInputFormat.java
index 8763493321..4505389174 100644
--- a/core/src/main/java/org/apache/carbondata/core/index/IndexInputFormat.java
+++ b/core/src/main/java/org/apache/carbondata/core/index/IndexInputFormat.java
@@ -59,6 +59,9 @@ import org.apache.log4j.Logger;
public class IndexInputFormat extends FileInputFormat<Void, ExtendedBlocklet>
implements Serializable, Writable {
+ // Keep attacker-controlled RPC allocations bounded. This matches Hadoop's default IPC limit.
+ static final int MAX_FILTER_RESOLVER_SIZE = 64 * 1024 * 1024;
+
private static final transient Logger LOGGER =
LogServiceFactory.getLogService(IndexInputFormat.class.getName());
@@ -317,10 +320,15 @@ public class IndexInputFormat extends FileInputFormat<Void, ExtendedBlocklet>
}
}
if (in.readBoolean()) {
- byte[] filterResolverBytes = new byte[in.readInt()];
+ int filterResolverSize = in.readInt();
+ if (filterResolverSize < 0 || filterResolverSize > MAX_FILTER_RESOLVER_SIZE) {
+ throw new IOException("Invalid filter resolver size: " + filterResolverSize);
+ }
+ byte[] filterResolverBytes = new byte[filterResolverSize];
in.readFully(filterResolverBytes, 0, filterResolverBytes.length);
- this.filterResolverIntf = (FilterResolverIntf) ObjectSerializationUtil
- .convertStringToObject(new String(filterResolverBytes, Charset.defaultCharset()));
+ this.filterResolverIntf = ObjectSerializationUtil.convertStringToObject(
+ new String(filterResolverBytes, Charset.defaultCharset()), FilterResolverIntf.class,
+ "org.apache.carbondata.");
}
this.indexToClear = in.readUTF();
this.taskGroupId = in.readUTF();
diff --git a/core/src/main/java/org/apache/carbondata/core/util/ObjectSerializationUtil.java b/core/src/main/java/org/apache/carbondata/core/util/ObjectSerializationUtil.java
index 9994d9cc25..5eea53e81d 100644
--- a/core/src/main/java/org/apache/carbondata/core/util/ObjectSerializationUtil.java
+++ b/core/src/main/java/org/apache/carbondata/core/util/ObjectSerializationUtil.java
@@ -20,8 +20,15 @@ package org.apache.carbondata.core.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -35,6 +42,16 @@ import org.apache.log4j.Logger;
*/
public class ObjectSerializationUtil {
+ private static final Set<String> SAFE_JAVA_CLASSES = Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(
+ "java.lang.Boolean", "java.lang.Byte", "java.lang.Character", "java.lang.Double",
+ "java.lang.Enum", "java.lang.Float", "java.lang.Integer", "java.lang.Long",
+ "java.lang.Number", "java.lang.Object", "java.lang.Short", "java.lang.String",
+ "java.math.BigDecimal", "java.math.BigInteger", "java.sql.Date",
+ "java.sql.Timestamp", "java.util.ArrayList", "java.util.Date", "java.util.HashMap",
+ "java.util.HashSet", "java.util.LinkedHashMap", "java.util.LinkedHashSet",
+ "java.util.LinkedList", "java.util.TreeMap", "java.util.TreeSet", "java.util.UUID")));
+
private static final Logger LOG =
LogServiceFactory.getLogService(ObjectSerializationUtil.class.getName());
@@ -116,4 +133,90 @@ public class ObjectSerializationUtil {
}
}
+ /**
+ * Converts a Base64 string to an object while restricting every class in the serialized graph.
+ * This method must be used when the serialized value came from an untrusted source such as RPC.
+ *
+ * @param objectString serialized object in string format
+ * @param expectedClass required type of the root object
+ * @param allowedClassPrefixes package prefixes allowed in the object graph
+ * @return the deserialized object
+ * @throws IOException if the stream contains a disallowed class or has an unexpected root type
+ */
+ public static <T> T convertStringToObject(String objectString, Class<T> expectedClass,
+ String... allowedClassPrefixes) throws IOException {
+ if (objectString == null) {
+ return null;
+ }
+
+ byte[] bytes = CarbonUtil.decodeStringToBytes(objectString);
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ GZIPInputStream gis = new GZIPInputStream(bais);
+ ObjectInputStream ois = new FilteringObjectInputStream(
+ Thread.currentThread().getContextClassLoader(), gis, allowedClassPrefixes)) {
+ Object object = ois.readObject();
+ if (!expectedClass.isInstance(object)) {
+ throw new InvalidClassException("Unexpected serialized type",
+ object == null ? "null" : object.getClass().getName());
+ }
+ return expectedClass.cast(object);
+ } catch (ClassNotFoundException e) {
+ throw new IOException("Could not read object", e);
+ }
+ }
+
+ private static final class FilteringObjectInputStream extends ClassLoaderObjectInputStream {
+
+ private final String[] allowedClassPrefixes;
+
+ private FilteringObjectInputStream(ClassLoader classLoader, GZIPInputStream inputStream,
+ String[] allowedClassPrefixes) throws IOException {
+ super(classLoader, inputStream);
+ this.allowedClassPrefixes = allowedClassPrefixes.clone();
+ }
+
+ @Override
+ protected Class<?> resolveClass(ObjectStreamClass descriptor)
+ throws IOException, ClassNotFoundException {
+ String className = getComponentClassName(descriptor.getName());
+ if (!isAllowed(className)) {
+ throw new InvalidClassException("Deserialization of class is not allowed", className);
+ }
+ return super.resolveClass(descriptor);
+ }
+
+ @Override
+ protected Class<?> resolveProxyClass(String[] interfaces) throws IOException {
+ // Proxy handlers are a common deserialization gadget entry point and are not needed here.
+ throw new InvalidClassException("Deserialization of proxy classes is not allowed",
+ Proxy.class.getName());
+ }
+
+ private boolean isAllowed(String className) {
+ if (isPrimitive(className) || SAFE_JAVA_CLASSES.contains(className)) {
+ return true;
+ }
+ for (String prefix : allowedClassPrefixes) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String getComponentClassName(String className) {
+ while (className.startsWith("[")) {
+ className = className.substring(1);
+ }
+ if (className.startsWith("L") && className.endsWith(";")) {
+ return className.substring(1, className.length() - 1);
+ }
+ return className;
+ }
+
+ private static boolean isPrimitive(String className) {
+ return className.length() == 1 && "ZBCSIJFD".contains(className);
+ }
+ }
+
}
diff --git a/core/src/test/java/com/attacker/EvilDeserializationPayload.java b/core/src/test/java/com/attacker/EvilDeserializationPayload.java
new file mode 100644
index 0000000000..f3d264d413
--- /dev/null
+++ b/core/src/test/java/com/attacker/EvilDeserializationPayload.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.attacker;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+
+public class EvilDeserializationPayload implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public static boolean executed;
+
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ executed = true;
+ input.defaultReadObject();
+ }
+}
diff --git a/core/src/test/java/org/apache/carbondata/core/util/ObjectSerializationUtilSecurityTest.java b/core/src/test/java/org/apache/carbondata/core/util/ObjectSerializationUtilSecurityTest.java
new file mode 100644
index 0000000000..c10984add5
--- /dev/null
+++ b/core/src/test/java/org/apache/carbondata/core/util/ObjectSerializationUtilSecurityTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.carbondata.core.util;
+
+import java.io.InvalidClassException;
+
+import org.apache.carbondata.core.metadata.datatype.DataTypes;
+import org.apache.carbondata.core.scan.expression.LiteralExpression;
+import org.apache.carbondata.core.scan.expression.conditional.EqualToExpression;
+import org.apache.carbondata.core.scan.filter.resolver.ConditionalFilterResolverImpl;
+import org.apache.carbondata.core.scan.filter.resolver.FilterResolverIntf;
+
+import com.attacker.EvilDeserializationPayload;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ObjectSerializationUtilSecurityTest {
+
+ @Test
+ public void testFilteredDeserializationAllowsFilterResolver() throws Exception {
+ FilterResolverIntf resolver = new ConditionalFilterResolverImpl(
+ new EqualToExpression(
+ new LiteralExpression("a", DataTypes.STRING),
+ new LiteralExpression("a", DataTypes.STRING)),
+ true, true, false);
+
+ String serialized = ObjectSerializationUtil.convertObjectToString(resolver);
+ FilterResolverIntf deserialized = ObjectSerializationUtil.convertStringToObject(
+ serialized, FilterResolverIntf.class, "org.apache.carbondata.");
+
+ Assert.assertTrue(deserialized instanceof ConditionalFilterResolverImpl);
+ }
+
+ @Test
+ public void testFilteredDeserializationRejectsClassBeforeReadObject() throws Exception {
+ EvilDeserializationPayload.executed = false;
+ String serialized = ObjectSerializationUtil.convertObjectToString(
+ new EvilDeserializationPayload());
+
+ try {
+ ObjectSerializationUtil.convertStringToObject(
+ serialized, FilterResolverIntf.class, "org.apache.carbondata.");
+ Assert.fail("Expected the attacker class to be rejected");
+ } catch (InvalidClassException expected) {
+ Assert.assertFalse(EvilDeserializationPayload.executed);
+ }
+ }
+}