Public cloneable() Method Without Final ('Object Hijack')
Description
Public cloneable() Method Without Final is a vulnerability where a class implements a cloneable() method that is not declared as final. This design flaw allows subclasses to override the clone method, potentially enabling object instantiation without invoking the proper constructor. Attackers can leverage this to create objects that bypass initialization logic, validation, or security checks that would normally occur in the constructor, leaving objects in unexpected or compromised states.
Risk
Non-final clone methods enable "object hijacking" attacks where attackers create malicious subclasses that override the clone method to return objects in invalid states or with manipulated internal data. This bypasses constructor-based security checks, input validation, and proper initialization sequences. In security-critical applications, this can allow creation of privileged objects without proper authorization, manipulation of authentication tokens, or circumvention of access controls. The risk is particularly severe for classes representing security credentials, financial transactions, or authorization contexts.
Solution
Declare the clone() method as final to prevent subclasses from overriding it. Alternatively, make the class itself final if appropriate. When implementing clone(), ensure it properly initializes all security-sensitive fields and maintains object invariants. Consider using copy constructors or factory methods as safer alternatives to cloning. If cloning must be supported, implement defensive copying and validate the cloned object's state. Use static analysis tools to identify classes with non-final clone methods.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Alter Execution Logic - Objects may exist in unexpected states that violate application assumptions about object initialization and consistency. |
| Access Control | Scope: Access Control Bypass Protection Mechanism - Attackers can bypass constructor-based security checks by instantiating objects through cloning. |
Example Code
Vulnerable Code
// Vulnerable: Cloneable class without final clone method
public class VulnerableBankAccount implements Cloneable {
private String accountNumber;
private double balance;
private boolean verified;
public VulnerableBankAccount(String accountNumber) {
// Constructor performs security validation
if (!validateAccountNumber(accountNumber)) {
throw new SecurityException("Invalid account number");
}
this.accountNumber = accountNumber;
this.balance = 0.0;
this.verified = false;
}
// Vulnerable: Not declared as final
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
private boolean validateAccountNumber(String number) {
// Validation logic
return number.matches("\\d{10}");
}
}
// Attacker can create malicious subclass
public class MaliciousBankAccount extends VulnerableBankAccount {
public MaliciousBankAccount(String accountNumber) {
super(accountNumber);
}
// Override clone to bypass validation
@Override
public Object clone() throws CloneNotSupportedException {
MaliciousBankAccount account = new MaliciousBankAccount("0000000000");
// Attacker can manipulate internal state
// using reflection or other techniques
return account;
}
}
// Vulnerable: Authentication token without final clone
public class VulnerableAuthToken implements Cloneable {
private String userId;
private String token;
private Set<String> permissions;
private Date expiration;
public VulnerableAuthToken(String userId) {
// Constructor verifies user exists
this.userId = verifyAndGetUser(userId);
this.token = generateSecureToken();
this.permissions = loadUserPermissions(userId);
this.expiration = calculateExpiration();
}
// Vulnerable: Non-final clone method
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
private String verifyAndGetUser(String userId) {
// Verify user exists in database
return userId;
}
private String generateSecureToken() {
// Generate cryptographically secure token
return UUID.randomUUID().toString();
}
private Set<String> loadUserPermissions(String userId) {
// Load from database
return new HashSet<>();
}
}
// Attacker exploits non-final clone
public class AuthTokenExploit extends VulnerableAuthToken {
public AuthTokenExploit(String userId) {
super(userId);
}
@Override
protected Object clone() throws CloneNotSupportedException {
// Bypass authentication entirely
// Return token with elevated privileges
AuthTokenExploit fakeToken = (AuthTokenExploit) super.clone();
// Use reflection to modify private fields
Field permField = AuthTokenExploit.class
.getSuperclass()
.getDeclaredField("permissions");
permField.setAccessible(true);
Set<String> adminPerms = new HashSet<>();
adminPerms.add("ADMIN");
adminPerms.add("READ_ALL");
adminPerms.add("WRITE_ALL");
permField.set(fakeToken, adminPerms);
return fakeToken;
}
}
Fixed Code
// Fixed: Final clone method prevents override
public class SecureBankAccount implements Cloneable {
private final String accountNumber;
private double balance;
private boolean verified;
public SecureBankAccount(String accountNumber) {
if (!validateAccountNumber(accountNumber)) {
throw new SecurityException("Invalid account number");
}
this.accountNumber = accountNumber;
this.balance = 0.0;
this.verified = false;
}
// Fixed: Declared as final - cannot be overridden
@Override
public final Object clone() throws CloneNotSupportedException {
SecureBankAccount cloned = (SecureBankAccount) super.clone();
// Ensure cloned object is in valid state
cloned.verified = false; // Require re-verification
return cloned;
}
private boolean validateAccountNumber(String number) {
return number.matches("\\d{10}");
}
}
// Fixed: Final class prevents subclassing entirely
public final class SecureAuthToken implements Cloneable {
private final String userId;
private final String token;
private final Set<String> permissions;
private final Date expiration;
public SecureAuthToken(String userId) {
this.userId = verifyAndGetUser(userId);
this.token = generateSecureToken();
this.permissions = Collections.unmodifiableSet(
loadUserPermissions(userId)
);
this.expiration = calculateExpiration();
}
// Fixed: Final class + final method = no override possible
@Override
public final Object clone() throws CloneNotSupportedException {
SecureAuthToken cloned = (SecureAuthToken) super.clone();
// Clone is safe because all fields are final/immutable
return cloned;
}
private String verifyAndGetUser(String userId) {
User user = userRepository.findById(userId);
if (user == null) {
throw new SecurityException("User not found");
}
return userId;
}
private String generateSecureToken() {
return TokenGenerator.generateSecure();
}
private Set<String> loadUserPermissions(String userId) {
return permissionService.getPermissions(userId);
}
private Date calculateExpiration() {
return Date.from(Instant.now().plusSeconds(3600));
}
}
// Fixed: Use copy constructor instead of clone
public final class SecureCredential {
private final String username;
private final byte[] hashedPassword;
private final Set<String> roles;
public SecureCredential(String username, byte[] hashedPassword,
Set<String> roles) {
this.username = Objects.requireNonNull(username);
this.hashedPassword = hashedPassword.clone(); // Defensive copy
this.roles = Collections.unmodifiableSet(new HashSet<>(roles));
}
// Fixed: Copy constructor instead of clone
public SecureCredential(SecureCredential original) {
this.username = original.username;
this.hashedPassword = original.hashedPassword.clone();
this.roles = original.roles; // Already unmodifiable
}
// Fixed: Factory method for creating copies
public static SecureCredential copyOf(SecureCredential original) {
return new SecureCredential(original);
}
// No clone() method - use copy constructor instead
}
// Fixed: If clone is needed, use defensive approach
public class DefensiveCloneExample implements Cloneable {
private String[] sensitiveData;
private Map<String, Object> config;
public DefensiveCloneExample() {
this.sensitiveData = new String[10];
this.config = new HashMap<>();
}
@Override
public final Object clone() throws CloneNotSupportedException {
DefensiveCloneExample cloned =
(DefensiveCloneExample) super.clone();
// Fixed: Deep copy mutable fields
cloned.sensitiveData = this.sensitiveData.clone();
cloned.config = new HashMap<>(this.config);
// Fixed: Validate cloned state
validateState(cloned);
return cloned;
}
private void validateState(DefensiveCloneExample obj) {
if (obj.sensitiveData == null || obj.config == null) {
throw new IllegalStateException("Invalid clone state");
}
}
}
CVE Examples
No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is documented in:
- CERT Oracle Secure Coding Standard for Java (OBJ07-J)
- Seven Pernicious Kingdoms taxonomy
References
- MITRE Corporation. "CWE-491: Public cloneable() Method Without Final ('Object Hijack')." https://cwe.mitre.org/data/definitions/491.html
- CERT Oracle Secure Coding Standard for Java. "OBJ07-J. Sensitive classes must not let themselves be copied."
- Oracle. "Secure Coding Guidelines for Java SE."