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);
+    }
+  }
+}