Skip to content

Commit

Permalink
Restrict Exception deserialization to Core and JDK classes
Browse files Browse the repository at this point in the history
when 3rd party exceptions are deserialized they might carry
classes not present on the deserializing node. This causes hard
exceptions and looses the exception entirely. This commit restricts
the classes we support for deserialization for core and selected JDK
classes to guarantee they are present on both source and target nodes
  • Loading branch information
s1monw committed Jul 13, 2015
1 parent 178aa26 commit bf3052d
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 6 deletions.
Expand Up @@ -19,9 +19,15 @@

package org.elasticsearch.common.io;

import com.fasterxml.jackson.core.JsonLocation;
import com.google.common.collect.ImmutableMap;
import org.elasticsearch.common.Classes;
import org.elasticsearch.common.collect.IdentityHashSet;
import org.joda.time.DateTimeFieldType;

import java.io.*;
import java.net.*;
import java.util.*;

/**
*
Expand Down Expand Up @@ -61,11 +67,11 @@ protected ObjectStreamClass readClassDescriptor()
case ThrowableObjectOutputStream.TYPE_STACKTRACEELEMENT:
return ObjectStreamClass.lookup(StackTraceElement.class);
case ThrowableObjectOutputStream.TYPE_FAT_DESCRIPTOR:
return super.readClassDescriptor();
return verify(super.readClassDescriptor());
case ThrowableObjectOutputStream.TYPE_THIN_DESCRIPTOR:
String className = readUTF();
Class<?> clazz = loadClass(className);
return ObjectStreamClass.lookup(clazz);
return verify(ObjectStreamClass.lookup(clazz));
default:
throw new StreamCorruptedException(
"Unexpected class descriptor type: " + type);
Expand Down Expand Up @@ -96,4 +102,40 @@ protected Class<?> loadClass(String className) throws ClassNotFoundException {
}
return clazz;
}

private static final Set<Class<?>> CLASS_WHITELIST;
private static final Set<Package> PKG_WHITELIST;
static {
IdentityHashSet<Class<?>> classes = new IdentityHashSet<>();
classes.add(String.class);
// inet stuff is needed for DiscoveryNode
classes.add(Inet6Address.class);
classes.add(Inet4Address.class);
classes.add(InetAddress.class);
classes.add(InetSocketAddress.class);
classes.add(SocketAddress.class);
classes.add(StackTraceElement.class);
classes.add(JsonLocation.class); // JsonParseException uses this
IdentityHashSet<Package> packages = new IdentityHashSet<>();
packages.add(Integer.class.getPackage()); // java.lang
packages.add(List.class.getPackage()); // java.util
packages.add(ImmutableMap.class.getPackage()); // com.google.common.collect
packages.add(DateTimeFieldType.class.getPackage()); // org.joda.time
CLASS_WHITELIST = Collections.unmodifiableSet(classes);
PKG_WHITELIST = Collections.unmodifiableSet(packages);
}

private ObjectStreamClass verify(ObjectStreamClass streamClass) throws IOException, ClassNotFoundException {
Class<?> aClass = resolveClass(streamClass);
Package pkg = aClass.getPackage();
if (aClass.isPrimitive() // primitives are fine
|| aClass.isArray() // arrays are ok too
|| Throwable.class.isAssignableFrom(aClass)// exceptions are fine
|| CLASS_WHITELIST.contains(aClass) // whitelist JDK stuff we need
|| PKG_WHITELIST.contains(aClass.getPackage())
|| pkg.getName().startsWith("org.elasticsearch")) { // es classes are ok
return streamClass;
}
throw new NotSerializableException(aClass.getName());
}
}
Expand Up @@ -19,10 +19,7 @@

package org.elasticsearch.common.io;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.*;

/**
*
Expand Down Expand Up @@ -65,4 +62,30 @@ protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException {
}
}
}

/**
* Simple helper method to roundtrip a serializable object within the ThrowableObjectInput/Output stream
*/
public static <T extends Serializable> T serialize(T t) throws IOException, ClassNotFoundException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try (ThrowableObjectOutputStream outputStream = new ThrowableObjectOutputStream(stream)) {
outputStream.writeObject(t);
}
try (ThrowableObjectInputStream in = new ThrowableObjectInputStream(new ByteArrayInputStream(stream.toByteArray()))) {
return (T) in.readObject();
}
}

/**
* Returns <code>true</code> iff the exception can be serialized and deserialized using
* {@link ThrowableObjectOutputStream} and {@link ThrowableObjectInputStream}. Otherwise <code>false</code>
*/
public static boolean canSerialize(Throwable t) {
try {
serialize(t);
return true;
} catch (Throwable throwable) {
return false;
}
}
}
Expand Up @@ -86,6 +86,10 @@ public void run() {
@Override
public void sendResponse(Throwable error) throws IOException {
BytesStreamOutput stream = new BytesStreamOutput();
if (ThrowableObjectOutputStream.canSerialize(error) == false) {
assert false : "Can not serialize exception: " + error; // make sure tests fail
error = new NotSerializableTransportException(error);
}
try {
writeResponseExceptionHeader(stream);
RemoteTransportException tx = new RemoteTransportException(targetTransport.nodeName(), targetTransport.boundAddress().boundAddress(), action, error);
Expand Down
Expand Up @@ -117,6 +117,10 @@ public void sendResponse(TransportResponse response, TransportResponseOptions op
@Override
public void sendResponse(Throwable error) throws IOException {
BytesStreamOutput stream = new BytesStreamOutput();
if (ThrowableObjectOutputStream.canSerialize(error) == false) {
assert false : "Can not serialize exception: " + error; // make sure tests fail
error = new NotSerializableTransportException(error);
}
try {
stream.skip(NettyHeader.HEADER_SIZE);
RemoteTransportException tx = new RemoteTransportException(transport.nodeName(), transport.wrapAddress(channel.getLocalAddress()), action, error);
Expand Down
98 changes: 98 additions & 0 deletions src/test/java/org/elasticsearch/ExceptionsSerializationTests.java
@@ -0,0 +1,98 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch;

import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import org.apache.lucene.store.AlreadyClosedException;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.io.ThrowableObjectInputStream;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.IndexShardState;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.indices.recovery.RecoveryFailedException;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.transport.ConnectTransportException;
import org.junit.Test;

import java.io.*;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.elasticsearch.common.io.ThrowableObjectOutputStream.serialize;

public class ExceptionsSerializationTests extends ElasticsearchTestCase {

public void testBasicExceptions() throws IOException, ClassNotFoundException {
ShardId id = new ShardId("foo", 1);
DiscoveryNode src = new DiscoveryNode("someNode", new InetSocketTransportAddress("127.0.0.1", 6666), Version.CURRENT);
DiscoveryNode target = new DiscoveryNode("otherNode", new InetSocketTransportAddress("127.0.0.1", 8888), Version.CURRENT);

RecoveryFailedException ex = new RecoveryFailedException(id, src, target, new AlreadyClosedException("closed", new SecurityException("booom booom boom", new FileNotFoundException("no such file"))));
RecoveryFailedException serialize = serialize(ex);
assertEquals(ex.getMessage(), serialize.getMessage());
assertEquals(AlreadyClosedException.class, serialize.getCause().getClass());
assertEquals(SecurityException.class, serialize.getCause().getCause().getClass());
assertEquals(FileNotFoundException.class, serialize.getCause().getCause().getCause().getClass());
ConnectTransportException tpEx = new ConnectTransportException(src, "foo", new IllegalArgumentException("boom"));
ConnectTransportException serializeTpEx = serialize(tpEx);
assertEquals(tpEx.getMessage(), serializeTpEx.getMessage());
assertEquals(src, tpEx.node());

TestException testException = new TestException(Arrays.asList("foo"), EnumSet.allOf(IndexShardState.class), ImmutableMap.<String,String>builder().put("foo", "bar").build(), InetAddress.getByName("localhost"), new Number[] {new Integer(1)});
assertEquals(serialize(testException).list.get(0), "foo");
assertTrue(serialize(testException).set.containsAll(Arrays.asList(IndexShardState.values())));
assertEquals(serialize(testException).map.get("foo"), "bar");
}

public void testPreventBogusFromSerializing() throws IOException, ClassNotFoundException {
Serializable[] serializables = new Serializable[] {
new AtomicBoolean(false),
TypeToken.of(String.class),
};
for (Serializable s : serializables) {
try {
serialize(s);
fail(s.getClass() + " should fail");
} catch (NotSerializableException e) {
// all is well
}
}
}

public static class TestException extends Throwable {
final List<String> list;
final EnumSet<IndexShardState> set;
final Map<String, String> map;
final InetAddress address;
final Object[] someArray;

public TestException(List<String> list, EnumSet<IndexShardState> set, Map<String, String> map, InetAddress address, Object[] someArray) {
super("foo", null);
this.list = list;
this.set = set;
this.map = map;
this.address = address;
this.someArray = someArray;
}
}
}

0 comments on commit bf3052d

Please sign in to comment.