Monday, May 25, 2015

Security In Java Serialization

Serialization turns Java object into a compact binary form which hardly read by ordinary people. This does not mean that the serialized form is encrypted safely. It can be read and analyzed by any determined person by referring to the Java Object Serialization Spec - Object Serialization Stream Protocol provided by Oracle. As a consequence, attacker is able to access the information in the serialized form or even makes up a fake binary form which could bypass the object validation rules that we enforced during the time we construct the object, and it will cause unexpected behavior in our software. You can read more about the how the attack can be done from the book Effective Java (2nd edition) Chapter 11.

class ApprovedLoanApplication implements Serializable {
    
    private static final long serialVersionUID = 1L;
    private static final double LEAST_ANNUAL_SALARY = 108000;
    
    private final String applicationId; // immutable field
    private final double annualSalary; // immutable field
    private final Date endDate;
    
    ApprovedLoanApplication(
        String applicationId, double annualSalary, Date endDate) {

        if (applicationId == null || applicationId.length() == 0) {
            throw new IllegalArgumentException(
                "Applicant Name is required.");
        }
        if (annualSalary < LEAST_ANNUAL_SALARY) {
            throw new IllegalArgumentException(
                "Least annual salary requirement not met. " 
                + "Loan application rejected.");
        }
        if (endDate == null) {
            throw new IllegalArgumentException("End Date is required.");
        }
        this.applicationId = applicationId;
        this.annualSalary = annualSalary;
        this.endDate = new Date(endDate.getTime()); //defensive copy
    }

    // other methods...
}

The ApprovedLoanApplication class above has done a good job in ensuring the object is valid and immutable during object construction. However, once this object is serialized into serialized form, the confidential data like annualSalary is visible even thought it been declared as private. One more issue is that, when the serialized form is deserialized, the same validation rules would not applied as the deserialization process will never call the class constructor in order to re-constitute back the object. This is one of the most important behavior of deserialization that we must aware of and react on it. It is causing the ApprovedLoanApplication facing security risks of data integrity breaking as the validated data could be hijacked and replaced with
  • modified data. For example, the attacker changes the mutable endDate to extends the loan period,
  • invalid data. For example, the attacker injects null applicationId which could cause system behave abnormally.

Encrypt serializable fields

To against unauthorized disclosure attack, we have to encrypt the confidential data before it is serialized. Encrypted data will be decrypted later during the deserialization process. Add the following methods into ApprovedLoanApplication.

private int encrypt(int figure) {
    // Encryption algorithm.
    // Could be replaced by complex encryption algorithm.
    return figure << 2; 
}

private int decrypt(int figure) {
    return figure >> 2; // Decryption algorithm.
}

private void writeObject(ObjectOutputStream out)
    throws IOException {
    
    this.annualSalary = encrypt(annualSalary); // encrypt
    out.defaultWriteObject();
}

private void readObject (ObjectInputStream in) 
    throws IOException, ClassNotFoundException {
   
    in.defaultReadObject();
    this.annualSalary = decrypt(annualSalary); //decrypted
}

Defensive copy

To against data modification attack, we have to perform defensive copy on our mutable object. Add the following code line into readObject() method.

this.endDate = new Date(endDate.getTime()); // defensive copy

Invariant checking

To against invariant breaking attack, we have to validate the data come from deserialization. As I mentioned earlier, deserialization does not invoke class constructor but instead, it will for sure invoke readObject() method, which give us a chance of managing the incoming stream data. Which means that we can do the same validation in the readObject() method. We may consider readObject() method is a constructor in serialization world. However, in this example, instead of duplicate the validation codes, I've offloaded the validation logic to a validator class which implements java.io.ObjectInputValidation. The validateObject() callback method will be called during deserialization runtime. Below is the full version after combining with data encryption and defensive copy techniques.

class ApprovedLoanApplication implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private final String applicationId;
    private int annualSalary; // can't be final due to encryption
    private Date endDate; // can't be final due to defensive copy

    //validator need not to be serialized
    private final transient ApprovedLoanApplicationValidator validator; 
    
    ApprovedLoanApplication(
        String applicationId, int annualSalary, Date endDate) {
        
        this.validator = 
            new ApprovedLoanApplicationValidator(applicationId, annualSalary, endDate);
        try {
            this.validator.validateObject(); // call the same validation logic
        } catch (InvalidObjectException ex) {
            throw new IllegalArgumentException(ex.getMessage());
        }
        this.applicationId = applicationId;
        this.annualSalary = annualSalary;
        this.endDate = new Date(endDate.getTime());
    }
    
    private int encrypt(int figure) {
        return figure << 2;
    }
    
    private int decrypt(int figure) {
        return figure >> 2;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        this.annualSalary = encrypt(annualSalary);
        out.defaultWriteObject();
    }
    
    private void readObject (ObjectInputStream in) 
        throws IOException, ClassNotFoundException {
        
        in.defaultReadObject();
        this.annualSalary = decrypt(annualSalary);
        this.endDate = new Date(endDate.getTime());
        in.registerValidation(new ApprovedLoanApplicationValidator(
            applicationId, annualSalary, endDate), 0); //register validator
    }
    
    // other methods...

    // Validator inner class which implements the validation logic
    private final class ApprovedLoanApplicationValidator 
        implements ObjectInputValidation {
        
        private static final int LEAST_ANNUAL_SALARY = 108000;

        private final String applicationId;
        private final int annualSalary;
        private final Date endDate;
        
        private ApprovedLoanApplicationValidator(
            String applicationId, int annualSalary, Date endDate) {

            this.applicationId = applicationId;
            this.annualSalary = annualSalary;
            this.endDate = endDate;
        }
        
        @Override
        public void validateObject() throws InvalidObjectException {
            if (applicationId == null || applicationId.length() == 0) {
                throw new IllegalArgumentException(
                    "Applicant Name is required.");
            }
            if (annualSalary &lt; LEAST_ANNUAL_SALARY) {
                throw new IllegalArgumentException(
                    "Least annual salary requirement not met."
                    + "Loan application rejected.");
            }
            if (endDate == null) {
                throw new IllegalArgumentException("End Date is required.");
            }
        }
    }   
}

Serialization Proxy

There is another way to prevent data integrity breaking by implementing serialization proxy pattern. This implementation basically takes the advantage of the fact that serialization allow us to serialize replacement object instead of the real object.


class ApprovedLoanApplication implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final double LEAST_ANNUAL_SALARY = 108000;
    
    private final String applicationId;
    private final int annualSalary; // maintain immutable
    private final Date endDate; // maintain immutable
    
    ApprovedLoanApplication(
        String applicationId, int annualSalary, Date endDate) {

        if (applicationId == null || applicationId.length() == 0) {
            throw new IllegalArgumentException(
            "Applicant Name is required.");
        }
        if (annualSalary &lt; LEAST_ANNUAL_SALARY) {
            throw new IllegalArgumentException(
            "Least annual salary requirement not met."
            + "Loan application rejected.");
        }
        if (endDate == null) {
            throw new IllegalArgumentException("End Date is required.");
        }
        this.applicationId = applicationId;
        this.annualSalary = annualSalary;
        this.endDate = new Date(endDate.getTime());
    }
    
    private Object writeReplace() throws ObjectStreamException {
        // serialize replacement object
        return new SerializationProxy(applicationId, annualSalary, endDate); 
    }
    
    private void readObject(ObjectInputStream in)
        throws InvalidObjectException {
        // Read data from stream in order to construct object's
        // fields in this method is no longer valid
        throw new InvalidObjectException("Proxy required."); 
    }

    // other methods...
    
    // static inner serializable proxy class
    private static class SerializationProxy implements Serializable { 
        private static final long serialVersionUID = 1L;
        private final String applicationId;
        private final int annualSalary;
        private final Date endDate;
        
        private SerializationProxy(
            String applicationId, int annualSalary, Date endDate) {

            this.applicationId = applicationId;
            this.annualSalary = encrypt(annualSalary);
            this.endDate = endDate;
        }
        
        private Object readResolve() throws ObjectStreamException {
            // return a new valid object
            return new ApprovedLoanApplication(
                applicationId, decrypt(annualSalary), endDate); 
        }
        
        private int encrypt(int figure) {
            return figure << 2;
        }

        private int decrypt(int figure) {
            return figure >> 2;
        }
    }
}

As you may aware that, using Serialization Proxy pattern, we mostly maintain our original code.  readResolve() method of a proxy class will create a new ApprovedLoanApplication object by calling the same class constructor, where the defensive copy and invariant checking are resided. Moreover, we could maintain the final declaration for ApprovedLoanApplication fields, which is not possible in the earlier techniques. This is very helpful because we could concentrate and remain on our class implementation instead of revising it to handle security in serialization.

In short, we should aware of the fact that, serialization does not provide security on itself. I even doubt that those techniques above may not enough to secure our objects, but at least those are best practices we should apply especially our serialized form contains sensitive information.

Reference:
Book: Effective Java 2nd Edition by Joshua Blosh

No comments: