CVE-2015-5377: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: Elasticsearch before 1.6.1 allows remote attackers to execute arbitrary code via unspecified vectors involving the transport protocol.  NOTE: ZDI appears to claim that CVE-2015-3253 and CVE-2015-5377 are the same vulnerability
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 9.8
Published: 2018-03-06T20:29Z
Repository: https://github.com/elastic/elasticsearch
Language: Java
Files changed: src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java, src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java, src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java, src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java, src/test/java/org/elasticsearch/ExceptionsSerializationTests.java
Commit date: 2015-07-13 17:39:29 +0200
Commit message: Restrict Exception deserialization to Core and JDK classes

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

Diff: diff --git a/src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java b/src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java
index b1dffd6d15..38ec79c47c 100644
--- a/src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java
+++ b/src/main/java/org/elasticsearch/common/io/ThrowableObjectInputStream.java
@@ -17,13 +17,19 @@
  * under the License.
  */
 
 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.*;
 
 /**
  *
  */
 public class ThrowableObjectInputStream extends ObjectInputStream {
@@ -59,15 +65,15 @@ public class ThrowableObjectInputStream extends ObjectInputStream {
             case ThrowableObjectOutputStream.TYPE_EXCEPTION:
                 return ObjectStreamClass.lookup(Exception.class);
             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);
         }
     }
@@ -94,6 +100,42 @@ public class ThrowableObjectInputStream extends ObjectInputStream {
         } else {
             clazz = Class.forName(className);
         }
         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());
+    }
 }
diff --git a/src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java b/src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java
index 282b2904dc..233965cb03 100644
--- a/src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java
+++ b/src/main/java/org/elasticsearch/common/io/ThrowableObjectOutputStream.java
@@ -17,14 +17,11 @@
  * under the License.
  */
 
 package org.elasticsearch.common.io;
 
-import java.io.IOException;
-import java.io.ObjectOutputStream;
-import java.io.ObjectStreamClass;
-import java.io.OutputStream;
+import java.io.*;
 
 /**
  *
  */
 public class ThrowableObjectOutputStream extends ObjectOutputStream {
@@ -63,6 +60,32 @@ public class ThrowableObjectOutputStream extends ObjectOutputStream {
                 write(TYPE_THIN_DESCRIPTOR);
                 writeUTF(desc.getName());
             }
         }
     }
+
+    /**
+     * 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;
+        }
+    }
 }
diff --git a/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java b/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java
index 243f92acd7..303a1e7331 100644
--- a/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java
+++ b/src/main/java/org/elasticsearch/transport/local/LocalTransportChannel.java
@@ -84,10 +84,14 @@ public class LocalTransportChannel implements TransportChannel {
     }
 
     @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);
             ThrowableObjectOutputStream too = new ThrowableObjectOutputStream(stream);
             too.writeObject(tx);
diff --git a/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java b/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java
index 0570dc5ef8..ee59063be9 100644
--- a/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java
+++ b/src/main/java/org/elasticsearch/transport/netty/NettyTransportChannel.java
@@ -115,10 +115,14 @@ public class NettyTransportChannel implements TransportChannel {
     }
 
     @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);
             ThrowableObjectOutputStream too = new ThrowableObjectOutputStream(stream);
             too.writeObject(tx);
diff --git a/src/test/java/org/elasticsearch/ExceptionsSerializationTests.java b/src/test/java/org/elasticsearch/ExceptionsSerializationTests.java
new file mode 100644
index 0000000000..34d98e44b1
--- /dev/null
+++ b/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;
+        }
+    }
+}

Vulnerable code:                 return super.readClassDescriptor();
                return ObjectStreamClass.lookup(clazz);
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
Fixed code: import com.fasterxml.jackson.core.JsonLocation;
import com.google.common.collect.ImmutableMap;
import org.elasticsearch.common.collect.IdentityHashSet;
import org.joda.time.DateTimeFieldType;
import java.net.*;
import java.util.*;
                return verify(super.readClassDescriptor());
                return verify(ObjectStreamClass.lookup(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());
    }
import java.io.*;

    /**
     * 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;
        }
    }
        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
            assert false : "Can not serialize exception: " + error; // make sure tests fail
            error = new NotSerializableTransportException(error);
        }
        if (ThrowableObjectOutputStream.canSerialize(error) == false) {
            assert false : "Can not serialize exception: " + error; // make sure tests fail
            error = new NotSerializableTransportException(error);
        }
/*
 * 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;
        }
    }
}