Continuing on ...

Lets say for example, we have a secure OS and we provide a service on a public port and we have a determined attacker attempting to use deserialization to take over our system or bring it to its knees with denial of service.

We know this is relatively easy with standard ObjectInputStream.

When we invoke a remote method, the return value is what you call a root object, that is it's the root of a serialized object tree, originating through the root object, via its fields.

Most Serializable objects have invariants.

We can consider the java serialization stream format as a string of commands that allows creation of any object, bypassing all levels of visibility. That is an attacker can create package private classes or private internal classes, provided they implement Serializable. So think of Serializable as a public constructor that lacks a context representing the attacker. That's right, there is no context representing the remote endpoint in the thread call stack.

Now we can place restrictions on the stream, such as a requirement that the stream is reset before a limit on the number of objects deserialized is reached. We can also place a limit on the count of all array elements read in from the stream, until the stream is reset. These measures ensure the stream cannot go on forever, they also prevent stack overflow and out of memory errors. At some point the caller will regain control of the stream without running out of resources.

But getting back to the previous problem, the attacker has command line access to create any Serializable object.

Now most objects have invariants that should be satisfied, for correct functional state, but a major problem with Serialization is it creates an instance, using the first zero arg constructor of the first non serializable superclass. Yep that's right, that could be ClassLoader, you clever attacker you! The poor old object hasn't even had a chance to check it's invariants and it's game over, that's not fair!

This will never do, I had to create a new contract for atomic object construction:

  1. The class must implement Serializable (can be inherited) AND one
     of the following conditions must be met:
  2. The class must be annotated with @AtomicSerial OR
  3. The class must be stateless and can be created by calling
     class.newInstance() OR
  4. The class must have DeSerializationPermission

Now if the class is annotated with @AtomicSerial, it must also have a constructor signature that has public or default visibility:

SomeClass(GetArg arg) throws IOException{
// The first thing I must do is to call a static method that checks invariants, before calling a superclass constructor, the static method should return the argument for another constructor.
}

Some simple rules for our object input stream:

  1. Though shalt not publish a reference to a partially constructed
     object.
  2. Though shalt not publish a reference if an object fails
     construction, where readObject, readObjectNoData and readResolve
     methods are considered to be constructors for the purpose of
     deserialization of conventional Serializable objects.
  3. Though shalt not attempt to construct a Serializable object that
     doesn't have an @AtomicSerial annotation, or has serial fields
     (state) and doesn't have DeSerializationPermission.
  4. Only call a protected constructor if the class doesn't implement
     @AtomicSerial and has DeSerializationPermission.
  5. Do not honour the standard java Serialization construction
     contract (not all Serializable classes can be constructed even if
     they have DeSerializationPermission).
  6. If a standard Serializable class has DeSerializationPermission for
     it to be constructed it must have a zero arg constructor or a
     constructor that accepts null object arguments and default
     primitive values.
  7. If an any object in a serialized object graph fails it's invariant
     checks, deserialization of the object graph fails at that point
     and control is returned to the caller, by way of an
     InvalidObjectException (a child class of IOException).
  8. Honour the standard java serialization stream protocol.
  9. Count number of objects received and throw a
     StreamCorruptedException if limit is exceeded.
 10. Count number of array elements received and throw
     StreamCorruptedException if limit is exceeded.
 11. readResolve() can be called on @AtomicSerial instances but,
     readObject() is never called and neither is readObjectNoData().

Obligations of our object output stream:

  1. Reset the stream before the limit is reached.
  2. Replace java collections and maps with safe @AtomicSerial
     implementations, it is the developers obligation to replace them
     with their preferred implementations during construction, these
     are functional but are only immutable containers for keys, values
     and comparators.
  3. Honour java's serial stream protocol.

Some simple rules for developers implementing @AtomicSerial:

  1. Check all invariants from a static method called by your class
     constructor prior to calling a superclass constructor.
  2. Check types of values in collections and maps and keys in maps.
  3. Check field types.
  4. Copy or clone arrays and collections.
  5. Do not call a super class constructor until all invariants have
     been checked.
  6. Handle failed invariants by throwing an InvalidObjectException,
     again make sure you did not call the super class constructor.
  7. Implement the interface ReadObject if you want to duplicate
     existing readObject method functionality, this will allow you to
     communicate with the stream prior to calling a super class
     constructor, so you can check invariants.  Annotate a static
     method that returns an instance of ReadObject with @ReadInput.
  8. Your ReadObject instance can be retrieved by from GetArg, your
     ReadObject instance will have read the stream prior to your
     constructor having been called.
  9. Don't call defaultReadObject() from your ReadObject implementation!
 10. Check all fields can be retrieved, prior to calling a super class
     constructor.
 11. Beware of the implicit super class constructor.
 12. Enum, and Proxy instances are considered secure, don't bother
     subclassing proxy, your class won't be deserialized.
 13. InvocationHandler's need to implement @AtomicSerial.

Note an object instance is not created until the default constructor in java.lang.Object is called.

Golden rules:

  1. Do not create arrays or collections blindly by reading in an
     integer or long, length or size from the stream to pass as an
     argument to an array or collection constructor.
  2. Let the stream be responsible for array creation.
  3. Clone or copy arrays and collections.
  4. Clone or copy mutable objects, these may be shared by other
     objects in the stream.
  5. Don't grant DeSerializationPermission unless you're really really
     sure you know what your doing, you must have access to the source
     code of the object you want to deserialize, you must make sure it
     is secure, if in doubt ask first.

Now before anyone goes off thinking "Oh no I don't need this functionality, do I have to use it?" The answer is no, it will be set via configuration constraints. If an endpoint doesn't support AtomicValidation, it will prevent ObjectInputStream creation. If an object in the stream doesn't support it, deserialization will fail and control will be returned to the caller.

Other than configuration, this is invisible to clients, only service api parameters and return values and private service classes, such as smart proxy's and proxy verifiers need implement it.

Note: I have tested both Reggie and Outrigger, standard serial form is preserved for all objects, all tests pass with AtomicValidation.YES enabled.

Clients will be able to be configured with InvocationConstraints; AtomicValidation.YES, Integrity.YES and Confidentiality.YES.

The client can also require DownloadPermission by specifying either of the properties:

net.jini.loader.ClassLoading.provider=net.jini.loader.pref.RequireDlPermProvider

OR

java.rmi.server.RMIClassLoaderSpi=net.jini.loader.pref.RequireDlPermProvider

If the first property is specified, it isn't required to be loaded by the system ClassLoader.

The next step is to provide a service interface for a bootstrap proxy to return a smart proxy and to define a suitable Entry for the bootstrap proxy to declare the interfaces it's smart proxy implements.

Clients can then lookup bootstrap proxy, based on the Entry, authenticate the bootstrap proxy and grant it DownloadPermission dynamically. The client will then need to prepare the smart proxy, placing constraints on it.

While the bootstrap proxy should have an AtomicVerification.YES constraint, the endpoint for the smart proxy doesn't have to, so after trust has been established, it is possible to run with standard serialization. In this case the smart proxy itself must be serializable using @AtomicSerial, but objects serialized over the smart proxy's endpoint's need not. I've also have made it possible for proxy codebases to declare the permissions they require so these can be granted dynamically after trust is established.

This would also provide the client with delayed unmarshalling functionality, while preserving the standard ServiceRegistrar interface, which can be managed using ServiceDiscoveryManager, and ServiceItemFilter.

In other words, this additional functionality requires minimal effort on behalf of the developer, while those who don't need it can remain blissfully ignorant and implement it later if they want to.

I haven't uploaded to svn, would you like to see some patches?

Regards,

Peter.

Reply via email to