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 8763493321e..45053891740 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 @@ public class IndexInputFormat extends FileInputFormat 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 void readFields(DataInput in) throws IOException { } } 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 9994d9cc25e..5eea53e81d5 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 @@ 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 @@ */ public class ObjectSerializationUtil { + private static final Set 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 static Object convertStringToObject(String objectString) throws IOExcepti } } + /** + * 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 convertStringToObject(String objectString, Class 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 00000000000..f3d264d413f --- /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 00000000000..c10984add52 --- /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); + } + } +}