diff --git a/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt b/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt new file mode 100644 index 0000000000..6f3bb8d3dd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt @@ -0,0 +1,125 @@ +package net.corda.core.serialization + +import sun.misc.Unsafe +import sun.security.util.Password +import java.io.* +import java.lang.invoke.* +import java.lang.reflect.* +import java.net.* +import java.security.* +import java.sql.Connection +import java.util.* +import java.util.logging.Handler +import java.util.zip.ZipFile +import kotlin.collections.HashSet +import kotlin.collections.LinkedHashSet + +/** + * This is a [ClassWhitelist] implementation where everything is whitelisted except for blacklisted classes and interfaces. + * In practice, as flows are arbitrary code in which it is convenient to do many things, + * we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint. + * Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour. + * Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well. + */ +object AllButBlacklisted : ClassWhitelist { + + private val blacklistedClasses = hashSetOf( + + // Known blacklisted classes. + Thread::class.java.name, + HashSet::class.java.name, + HashMap::class.java.name, + ClassLoader::class.java.name, + Handler::class.java.name, // MemoryHandler, StreamHandler + Runtime::class.java.name, + Unsafe::class.java.name, + ZipFile::class.java.name, + Provider::class.java.name, + SecurityManager::class.java.name, + Random::class.java.name, + + // Known blacklisted interfaces. + Connection::class.java.name, + // TODO: AutoCloseable::class.java.name, + + // java.security. + KeyStore::class.java.name, + Password::class.java.name, + AccessController::class.java.name, + Permission::class.java.name, + + // java.net. + DatagramSocket::class.java.name, + ServerSocket::class.java.name, + Socket::class.java.name, + URLConnection::class.java.name, + // TODO: add more from java.net. + + // java.io. + Console::class.java.name, + File::class.java.name, + FileDescriptor::class.java.name, + FilePermission::class.java.name, + RandomAccessFile::class.java.name, + Reader::class.java.name, + Writer::class.java.name, + // TODO: add more from java.io. + + // java.lang.invoke classes. + CallSite::class.java.name, // for all CallSites eg MutableCallSite, VolatileCallSite etc. + LambdaMetafactory::class.java.name, + MethodHandle::class.java.name, + MethodHandleProxies::class.java.name, + MethodHandles::class.java.name, + MethodHandles.Lookup::class.java.name, + MethodType::class.java.name, + SerializedLambda::class.java.name, + SwitchPoint::class.java.name, + + // java.lang.invoke interfaces. + MethodHandleInfo::class.java.name, + + // java.lang.invoke exceptions. + LambdaConversionException::class.java.name, + WrongMethodTypeException::class.java.name, + + // java.lang.reflect. + AccessibleObject::class.java.name, // For Executable, Field, Method, Constructor. + Modifier::class.java.name, + Parameter::class.java.name, + ReflectPermission::class.java.name + // TODO: add more from java.lang.reflect. + ) + + // Specifically exclude classes from the blacklist, + // even if any of their superclasses and/or implemented interfaces are blacklisted. + private val forciblyAllowedClasses = hashSetOf( + LinkedHashSet::class.java.name, + LinkedHashMap::class.java.name, + InputStream::class.java.name, + BufferedInputStream::class.java.name, + Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream").name + ) + + /** + * This implementation supports inheritance; thus, if a superclass or superinterface is blacklisted, so is the input class. + */ + override fun hasListed(type: Class<*>): Boolean { + // Check if excluded. + if (type.name !in forciblyAllowedClasses) { + // Check if listed. + if (type.name in blacklistedClasses) + throw IllegalStateException("Class ${type.name} is blacklisted, so it cannot be used in serialization.") + // Inheritance check. + else { + val aMatch = blacklistedClasses.firstOrNull { Class.forName(it).isAssignableFrom(type) } + if (aMatch != null) { + // TODO: blacklistedClasses += type.name // add it, so checking is faster next time we encounter this class. + val matchType = if (Class.forName(aMatch).isInterface) "superinterface" else "superclass" + throw IllegalStateException("The $matchType $aMatch of ${type.name} is blacklisted, so it cannot be used in serialization.") + } + } + } + return true + } +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt index 821b7e784f..d9755cec29 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -25,6 +25,10 @@ fun makeNoWhitelistClassResolver(): ClassResolver { return CordaClassResolver(AllWhitelist) } +fun makeAllButBlacklistedClassResolver(): ClassResolver { + return CordaClassResolver(AllButBlacklisted) +} + class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() { /** Returns the registration for the specified class, or null if the class is not registered. */ override fun getRegistration(type: Class<*>): Registration? { @@ -55,8 +59,9 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() return checkClass(type.superclass) } // It's safe to have the Class already, since Kryo loads it with initialisation off. - val hasAnnotation = checkForAnnotation(type) - if (!hasAnnotation && !whitelist.hasListed(type)) { + // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw a NotSerializableException if input class is blacklisted. + // Thus, blacklisting precedes annotation checking. + if (!whitelist.hasListed(type) && !checkForAnnotation(type)) { throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization") } return null diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 5a68d5a5c5..16e73c836b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -33,7 +33,6 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.security.spec.InvalidKeySpecException import java.time.Instant import java.util.* @@ -460,7 +459,7 @@ object KotlinObjectSerializer : Serializer() { } // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. -private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeNoWhitelistClassResolver())) }.build() +private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build() private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build() // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. diff --git a/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt index 0ccad7ade3..46d4aa7499 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt @@ -8,7 +8,12 @@ import net.corda.core.node.AttachmentClassLoaderTests import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.services.AttachmentStorage import net.corda.testing.node.MockAttachmentStorage +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import java.lang.IllegalStateException +import java.sql.Connection +import java.util.* @CordaSerializable enum class Foo { @@ -160,4 +165,76 @@ class CordaClassResolverTests { CordaClassResolver(EmptyWhitelist).getRegistration(SubSubElement::class.java) CordaClassResolver(EmptyWhitelist).getRegistration(SerializableViaSuperSubInterface::class.java) } + + // Blacklist tests. + @get:Rule + val expectedEx = ExpectedException.none()!! + + @Test + fun `Check blacklisted class`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("Class java.util.HashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // HashSet is blacklisted. + resolver.getRegistration(HashSet::class.java) + } + + open class SubHashSet : HashSet() + @Test + fun `Check blacklisted subclass`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$SubHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubHashSet extends the blacklisted HashSet. + resolver.getRegistration(SubHashSet::class.java) + } + + class SubSubHashSet : SubHashSet() + @Test + fun `Check blacklisted subsubclass`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$SubSubHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubSubHashSet extends SubHashSet, which extends the blacklisted HashSet. + resolver.getRegistration(SubSubHashSet::class.java) + } + + class ConnectionImpl(val connection: Connection) : Connection by connection + @Test + fun `Check blacklisted interface impl`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.core.serialization.CordaClassResolverTests\$ConnectionImpl is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // ConnectionImpl implements blacklisted Connection. + resolver.getRegistration(ConnectionImpl::class.java) + } + + interface SubConnection : Connection + class SubConnectionImpl(val subConnection: SubConnection) : SubConnection by subConnection + @Test + fun `Check blacklisted super-interface impl`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.core.serialization.CordaClassResolverTests\$SubConnectionImpl is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubConnectionImpl implements SubConnection, which extends the blacklisted Connection. + resolver.getRegistration(SubConnectionImpl::class.java) + } + + @Test + fun `Check forcibly allowed`() { + val resolver = CordaClassResolver(AllButBlacklisted) + // LinkedHashSet is allowed for serialization. + resolver.getRegistration(LinkedHashSet::class.java) + } + + @CordaSerializable + class CordaSerializableHashSet : HashSet() + @Test + fun `Check blacklist precedes CordaSerializable`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$CordaSerializableHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // CordaSerializableHashSet is @CordaSerializable, but extends the blacklisted HashSet. + resolver.getRegistration(CordaSerializableHashSet::class.java) + } } \ No newline at end of file