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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
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());

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -35,6 +42,16 @@
*/
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());

Expand Down Expand Up @@ -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> 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);
}
}

}
34 changes: 34 additions & 0 deletions core/src/test/java/com/attacker/EvilDeserializationPayload.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading