Thank you all for participating in this discussion.
Initially a constructor signature for deserialization was proposed to
enforce invariants and encapsulation, however there appears to be
interest in using alternative methods, although they appear to be
improvements over the status quo, I'm having trouble working out how to:
* Enforce intra class invariants with alternate proposals without
breaking encapsulation.
* How the static method proposal can be used to replace final fields
referents without needing to implement readObject().
* And how the alternative GetFields readObject() implementation can
enforce invariants and prevent finalizer attack, while also
allowing subclassing?
What started me down this path, is our project (Apache River) is heavily
dependant on Serialization, I wanted to create a secure
ObjectInputStream subclass that restricted deserialized classes to those
I had audited. My aim was to allow deserialization to enforce security
using verification, similar to how an http server verifies its input.
Unfortunately before I can achieve the goal of secure deserialization,
there's a denial of service issue in ObjectInputStream: I'd also like to
propose a system property, to allow limiting the size of arrays, created
during deserialization. Presently I can craft an inputstream to take
down the JVM, if I can do it, so can an attacker, I'd like to get that
fixed if possible.
I've attached a text file that contains three classes using the original
proposed serial constructor, A, B and C which have intra class
invariants that must be satiisfied. C also contains a circular link and
implements an interface called Circular, the Serialization framework
calls it after construction to provide access to fields with circular
links. Can the other proposals provide similar safety?
Notes:
1. The class with the circular link is final and every method checks
if invariants are satisfied.
2. The method with @SerialConstructor annotation, is the only
constructor called by the serialization frame work, other than the
Circular interface method, there are no other methods that need to
be implemented.
3. Encapsulation is preserved, classes have full control over their
invariants, their internal implementation and what's serialized.
Regards,
Peter.
// A simple complete example of safely constructed explicit Serialization.
//
// No special framework requirements, other than @CallerSensitive ReadSerial
// to preserve encapsulation.
// Object's serial form can add or remove fields without breaking encapsulation
or
// causing compatibility errors.
// When a field is not present in the stream, the default value is returned
// and the programmers invariant checking code does the rest.
// Classes can implement finalizer method if they want, no need to
// disallow it.
// Programmer only uses public api, IDE friendly.
// Class inheritance heirarchy can be changed and still all invariants can be
satisfied.
// Public api is unit test friendly.
// You can't do intra class invariant checks with readObject() without breaking
// encapsulation.
//
// It is not possible to construct an object that doesn't satisfy invariants.
//
// Easy to audit for security.
public class A implements Serializable {
private final int lower, upper;
static boolean check(int lower, int upper){
if (upper < lower) throw new IllegalArgumentException(
“upper limit cannot be less than lower”);
return true;
}
public A(int lower, int upper){
this(check(lower,upper),lower, upper);
}
@SerialConstructor
public A(ReadSerial rs){
this(rs.getInt("lower"), rs.getInt("upper"));
}
private A(boolean checked, int lower, int upper){
this.lower = lower;
this.upper = upper;
}
public final int lowerLimit(){
return lower;
}
public final int upperLimit(){
return upper;
}
}
public class B extends A {
private final cur;
static boolean check(int lower, int cur, int upper){
if (cur < lower || cur > upper)
throw new IllegalArgumentException("cur out of bounds");
return true;
}
static boolean check(ReadSerial rs){
A a = new A(rs);
return check(a.lowerLimit(), rs.getInt("cur"),
a.getUpperLimit());
}
public B(int lower, int cur, int upper){
this(check(lower, cur, upper), lower, cur, upper);
}
private B(boolean check, int lower, int cur, int upper){
super(lower, upper);
this.cur = cur;
}
@SerialConstructor
public B(ReadSerial rs){
this(check(rs), rs);
}
private B(boolean check, ReadSerial rs){
super(rs);
cur = rs.getInt("cur");
}
public final int getCur(){
return cur;
}
}
// For Circular links, implementing object declares method final
// and if extension is allowed, provides a protected
// method for subclasses to optionally implement.
//
// Class is final to prevent finalizer attack or methods
// being overridden.
//
// Due to the circular link, some invariants can't be
// checked until after construction, this makes invariants
// much more difficult to enforce.
public interface Circular {
public void setMutableFields(ReadSerial rs);
}
public final class C extends B implements Circular {
private final B [] circularArray; //Has reference to all members and is
a member.
private final int number;
private transient boolean safe;
static boolean check(int lower, int upper, int number){
if (number < upper - lower)
throw new IllegalArgumentException("insufficent array length");
if (number > 512)
throw new IllegalArgumentException("number exceeds allowed
limit");
}
static boolean check(ReadSerial rs){
A a = new A(rs);
return check(a.lowerLimit(), a.upperLimit(),
rs.getInt("number"));
}
public C (int lower, int cur, int upper, int number){
this(check(lower, upper, number), lower, cur, upper, number);
}
private C(boolean check, int lower, int cur, int upper, int number){
super(lower, cur, upper);
this.number = number;
circularArray = new B[number];
circularArray[cur] = this;
safe = true;
}
public C(ReadSerial rs){
this(check(rs), rs);
}
private C(boolean check, ReadSerial rs){
super(rs);
number = rs.getInt("number");
circularArray = new B[number];
safe = false;
}
public B set(B b){
int cur = b.getCur();
B previous;
synchronized (circularArray){
if (!safe) throw new IllegalArgumentException("object
incorrectly constructed");
previous = circularArray[cur];
circularArray[cur] = b;
}
return previous;
}
public void setMutableFields(ReadSerial rs){
synchronized(circularArray){
if (safe) throw new IllegalArgumentException("fields
must be set using public api");
B [] circular = rs.getArray(new B [number]);
if ( circular[getCur()] != this){
safe = false;
throw new IllegalArgumentException("incorrect
position cur in array");
}
for (int i = 0; i < number; i++){
if (circular[i] != null && circular[i].getCur()
!= i){
safe = false;
throw new
IllegalArgumentException("incorrect position cur in array");
}
}
safe = true;
}
}
}