Blacklist implementation for internal Kryo

Blacklist support for internal Kryo, supporting inheritance and forciblyAllowed classes.
This commit is contained in:
Konstantinos Chalkias 2017-06-13 17:39:54 +01:00 committed by GitHub
parent a9b794ace5
commit 56bad3a9b4
4 changed files with 210 additions and 4 deletions

View File

@ -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<String>(
// 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<String>(
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
}
}

View File

@ -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

View File

@ -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<DeserializeAsKotlinObjectDef>() {
}
// 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.

View File

@ -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<E> : HashSet<E>()
@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<E> : SubHashSet<E>()
@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<E> : HashSet<E>()
@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)
}
}