Serializable Class Containing Sensitive Data

Description

Serializable Class Containing Sensitive Data is a vulnerability where a class containing sensitive information does not explicitly prevent serialization, allowing attackers to extract the data by serializing the class through another class. Serializable classes are effectively open classes since data cannot be hidden in them - any code with access to an instance can serialize it to a byte stream, transmit or store it, and extract the contained data. This bypasses the normal access controls and encapsulation that protect sensitive information during runtime.

Risk

Serialization of sensitive data creates severe confidentiality and integrity risks. Attackers can write sensitive objects to byte streams to extract passwords, cryptographic keys, personal information, or financial data. Serialized data can be captured in transit, stored in logs, or extracted from memory dumps. The serialized form exposes private fields that would normally be protected by access modifiers. Deserialization can also be exploited to inject modified sensitive data back into the application. In distributed systems, serialization may inadvertently transmit sensitive data to untrusted components.

Solution

Prevent serialization of classes containing sensitive data by defining a final writeObject() method that throws NotSerializableException. Alternatively, implement Externalizable and throw exceptions in the required methods. Mark sensitive fields as transient so they are excluded from serialization. If serialization is required, encrypt sensitive fields before serialization and decrypt after deserialization. Use serialization filters (Java 9+) to validate serialized objects. Consider using secure alternatives to Java serialization like JSON with explicit field mapping that excludes sensitive data.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - An attacker can write the class to a byte stream and extract important data from it, gaining unauthorized access to sensitive information like credentials, keys, or personal data.
IntegrityScope: Integrity

Modify Application Data - Attackers can modify serialized data and deserialize it back into the application, potentially corrupting sensitive state or bypassing security checks.

Example Code

Vulnerable Code

// Vulnerable: Serializable class with sensitive data
import java.io.*;

public class VulnerablePatientRecord implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String patientId;
    private String socialSecurityNumber;  // Highly sensitive!
    private String medicalHistory;        // Protected health information
    private String diagnosis;
    private byte[] geneticData;           // Extremely sensitive

    public VulnerablePatientRecord(String name, String ssn) {
        this.name = name;
        this.socialSecurityNumber = ssn;
    }

    // No protection against serialization!
    // Any code can serialize and extract all fields
}

// Attacker extracts sensitive data
public class PatientDataExploit {
    public void extractData(VulnerablePatientRecord patient)
            throws Exception {

        // Serialize the patient record
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(patient);
        oos.close();

        // Attacker now has byte array with all sensitive data
        byte[] serializedData = baos.toByteArray();

        // Can be written to file, sent over network, etc.
        Files.write(Paths.get("/tmp/stolen_patient_data.ser"), serializedData);

        // Or extract fields directly
        ObjectInputStream ois = new ObjectInputStream(
            new ByteArrayInputStream(serializedData)
        );
        VulnerablePatientRecord stolen =
            (VulnerablePatientRecord) ois.readObject();

        // Access private fields through reflection or getter
        System.out.println("SSN: " + stolen.getSocialSecurityNumber());
    }
}

// Vulnerable: User credentials serializable
public class VulnerableUserSession implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private String passwordHash;
    private String authToken;
    private byte[] sessionKey;
    private Date loginTime;
    private Set<String> permissions;

    // All sensitive session data can be serialized!
}

// Vulnerable: Serializable through inheritance
public class VulnerableSecureData extends SerializableBase {
    // Inherits Serializable from parent
    private String apiKey;
    private String secretToken;
    private PrivateKey signingKey;

    // Even without explicit Serializable, parent makes it serializable
}

// Vulnerable: Serializable by default in some frameworks
@Entity  // JPA entities are often serializable
public class VulnerableUser implements Serializable {
    @Id
    private Long id;

    private String username;

    @Column(name = "password_hash")
    private String passwordHash;  // Sensitive!

    @Column(name = "security_question")
    private String securityQuestion;

    @Column(name = "security_answer")
    private String securityAnswer;  // Sensitive!
}
// Vulnerable: Financial data
public class VulnerableBankAccount implements Serializable {
    private static final long serialVersionUID = 1L;

    private String accountNumber;
    private String routingNumber;
    private double balance;
    private String pin;           // Very sensitive!
    private List<Transaction> transactionHistory;

    // Serialization exposes all financial data
}

// Attacker can intercept and modify
public class FinancialExploit {
    public void manipulateAccount(byte[] serializedAccount) throws Exception {
        // Deserialize
        ObjectInputStream ois = new ObjectInputStream(
            new ByteArrayInputStream(serializedAccount)
        );
        VulnerableBankAccount account =
            (VulnerableBankAccount) ois.readObject();

        // Use reflection to modify balance
        Field balanceField = account.getClass().getDeclaredField("balance");
        balanceField.setAccessible(true);
        balanceField.setDouble(account, 1000000.0);  // Inflate balance!

        // Re-serialize with modified data
        // ... send back to application
    }
}

Fixed Code

// Fixed: Prevent serialization of sensitive class
import java.io.*;

public final class SecurePatientRecord implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String patientId;

    // Fixed: Transient fields are not serialized
    private transient String socialSecurityNumber;
    private transient String medicalHistory;
    private transient byte[] geneticData;

    // Fields that can be serialized (non-sensitive)
    private String diagnosis;
    private Date visitDate;

    public SecurePatientRecord(String name, String ssn) {
        this.name = name;
        this.socialSecurityNumber = ssn;
    }

    // Fixed: Throw exception on serialization attempt
    private void writeObject(ObjectOutputStream out)
            throws NotSerializableException {
        throw new NotSerializableException(
            "Patient records cannot be serialized"
        );
    }

    // Fixed: Also prevent deserialization
    private void readObject(ObjectInputStream in)
            throws NotSerializableException {
        throw new NotSerializableException(
            "Patient records cannot be deserialized"
        );
    }
}

// Fixed: Alternative - encrypt sensitive data during serialization
public class SecureEncryptedRecord implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String patientId;

    // Fixed: Store encrypted, not plaintext
    private byte[] encryptedSSN;
    private byte[] encryptedMedicalHistory;

    // Fixed: Transient - never serialize
    private transient String plaintextSSN;
    private transient String plaintextMedicalHistory;

    public SecureEncryptedRecord(String name, String ssn, Key encryptionKey) {
        this.name = name;
        this.plaintextSSN = ssn;
        this.encryptedSSN = encrypt(ssn, encryptionKey);
    }

    // Fixed: Custom serialization with encryption
    private void writeObject(ObjectOutputStream out) throws IOException {
        // Ensure sensitive data is encrypted before writing
        if (plaintextSSN != null && encryptedSSN == null) {
            throw new IOException("SSN must be encrypted before serialization");
        }
        out.defaultWriteObject();

        // Clear transient field
        plaintextSSN = null;
    }

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // plaintextSSN remains null until explicitly decrypted
    }

    public String getSSN(Key decryptionKey) {
        if (plaintextSSN == null && encryptedSSN != null) {
            plaintextSSN = decrypt(encryptedSSN, decryptionKey);
        }
        return plaintextSSN;
    }

    private byte[] encrypt(String data, Key key) {
        // Use AES-GCM or similar
        return EncryptionUtil.encrypt(data.getBytes(), key);
    }

    private String decrypt(byte[] data, Key key) {
        return new String(EncryptionUtil.decrypt(data, key));
    }
}

// Fixed: Use Externalizable for complete control
public final class SecureUserSession implements Externalizable {
    private String username;
    private transient String passwordHash;
    private transient String authToken;
    private transient byte[] sessionKey;
    private Date loginTime;

    // Required no-arg constructor for Externalizable
    public SecureUserSession() {}

    // Fixed: Explicitly deny externalization
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        throw new IOException("User sessions cannot be serialized");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        throw new IOException("User sessions cannot be deserialized");
    }
}

// Fixed: JPA entity with protected serialization
@Entity
public class SecureUser implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private Long id;

    private String username;

    // Fixed: Transient - not serialized
    @Transient
    private transient String passwordHash;

    // Fixed: Stored encrypted in database, transient for serialization
    @Column(name = "encrypted_security_answer")
    private String encryptedSecurityAnswer;

    @Transient
    private transient String securityAnswer;

    // Fixed: Prevent Java serialization
    private void writeObject(ObjectOutputStream out)
            throws NotSerializableException {
        throw new NotSerializableException("Users cannot be serialized");
    }

    // Fixed: Safe DTO conversion
    public UserDTO toDTO() {
        UserDTO dto = new UserDTO();
        dto.setId(id);
        dto.setUsername(username);
        // Never include sensitive data in DTO
        return dto;
    }
}

// Fixed: Bank account with serialization protection
public final class SecureBankAccount implements Serializable {
    private static final long serialVersionUID = 1L;

    private String accountNumber;
    private String routingNumber;

    // Fixed: Sensitive fields are transient
    private transient double balance;
    private transient String pin;
    private transient List<Transaction> transactionHistory;

    // Fixed: Throw on any serialization attempt
    private void writeObject(ObjectOutputStream out)
            throws NotSerializableException {
        throw new NotSerializableException(
            "Bank accounts cannot be serialized for security reasons"
        );
    }

    private void readObject(ObjectInputStream in)
            throws NotSerializableException {
        throw new NotSerializableException(
            "Bank accounts cannot be deserialized"
        );
    }

    // Fixed: If transfer is needed, use secure DTO
    public AccountSummaryDTO toSummary() {
        return new AccountSummaryDTO(
            maskAccountNumber(accountNumber),
            balance  // Balance shown only in authenticated context
        );
    }

    private String maskAccountNumber(String account) {
        return "****" + account.substring(account.length() - 4);
    }
}

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is related to:

  • Java deserialization vulnerabilities (CWE-502)
  • CERT Secure Coding Standard for Java (SER00-J)

References

  1. MITRE Corporation. "CWE-499: Serializable Class Containing Sensitive Data." https://cwe.mitre.org/data/definitions/499.html
  2. CERT Oracle Secure Coding Standard for Java. "SER03-J. Do not serialize unencrypted sensitive data."
  3. Oracle. "Secure Coding Guidelines for Java SE - Serialization."