Comparison of Object References Instead of Object Contents

Description

Comparison of Object References Instead of Object Contents occurs when code uses reference equality operators (== or !=) to compare objects when it should compare their values using equals() or similar methods. In languages like Java, == compares memory addresses (whether two references point to the same object), not the actual content of objects. This leads to unexpected behavior when logically equal objects are considered different.

Risk

Reference comparison bugs cause authentication bypasses when string passwords are compared incorrectly. Business logic fails when equivalent objects aren't recognized as equal. Security tokens may not validate properly. Caching fails when equal objects aren't detected. Collections behave unexpectedly. The code may work with some inputs (due to string interning or boxing caches) but fail with others.

Solution

Use equals() method for object comparison in Java. Use Objects.equals() for null-safe comparison. Override equals() and hashCode() when creating value classes. In JavaScript, understand the difference between == and === for objects. Be aware of language-specific comparison semantics. Use appropriate comparison methods for strings, numbers, and custom objects.

Common Consequences

ImpactDetails
LogicScope: Incorrect Behavior

Logically equal objects treated as different.
SecurityScope: Authentication Bypass

Credential comparison may fail or succeed incorrectly.
ReliabilityScope: Inconsistent Results

Code may work sometimes due to caching/interning.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: String comparison with ==
public class VulnerableAuth {

    public boolean authenticate(String inputPassword, String storedPassword) {
        // BUG: Compares references, not content!
        return inputPassword == storedPassword;
    }

    public boolean checkRole(String userRole, String requiredRole) {
        // May work sometimes due to string interning, fail other times
        return userRole == requiredRole;
    }
}

// VULNERABLE: Integer comparison with ==
public class VulnerableIdCheck {

    public boolean isSameUser(Integer userId1, Integer userId2) {
        // BUG: Works for -128 to 127 (cached), fails for larger values!
        return userId1 == userId2;
    }

    public void demonstrateProblem() {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);  // true (cached)

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);  // false! (not cached)
    }
}

// VULNERABLE: Custom object comparison
public class User {
    private Long id;
    private String name;

    // No equals() override - uses Object.equals (reference comparison)
}

public class VulnerableUserService {

    public boolean isSameUser(User user1, User user2) {
        // BUG: Compares references, not content!
        return user1 == user2;
    }

    public boolean containsUser(List<User> users, User target) {
        for (User u : users) {
            if (u == target) {  // BUG: Reference comparison
                return true;
            }
        }
        return false;
    }
}

// VULNERABLE: Token validation
public class VulnerableTokenValidator {

    public boolean validateToken(String providedToken, String expectedToken) {
        // BUG: May fail even with correct token!
        return providedToken == expectedToken;
    }
}

// VULNERABLE: Enum-like class comparison
public class Status {
    public static final Status ACTIVE = new Status("ACTIVE");
    public static final Status INACTIVE = new Status("INACTIVE");

    private String name;

    public Status(String name) {
        this.name = name;
    }
}

public class VulnerableStatusCheck {

    public boolean isActive(Status status) {
        // BUG: new Status("ACTIVE") == Status.ACTIVE is false!
        return status == Status.ACTIVE;
    }
}
// VULNERABLE: Boolean comparison
public class VulnerableBooleanCheck {

    public boolean checkFlag(Boolean flag) {
        // BUG: May fail for Boolean objects!
        return flag == Boolean.TRUE;
    }

    public void demonstrate() {
        Boolean b1 = Boolean.TRUE;
        Boolean b2 = new Boolean(true);  // Different object!

        System.out.println(b1 == Boolean.TRUE);  // true
        System.out.println(b2 == Boolean.TRUE);  // false!
    }
}

// VULNERABLE: Array comparison
public class VulnerableArrayCheck {

    public boolean sameArray(byte[] arr1, byte[] arr2) {
        // BUG: Compares references, not array contents!
        return arr1 == arr2;
    }
}

// VULNERABLE: Date comparison
public class VulnerableDateCheck {

    public boolean isSameDate(Date date1, Date date2) {
        // BUG: Reference comparison!
        return date1 == date2;
    }
}
// VULNERABLE: JavaScript object comparison
function vulnerableUserCheck(user1, user2) {
    // BUG: Objects with same content are not ===
    return user1 === user2;
}

// Demonstrates the issue
const a = { name: "Alice" };
const b = { name: "Alice" };
console.log(a === b);  // false!

// VULNERABLE: Array comparison
function vulnerableArrayCheck(arr1, arr2) {
    // BUG: Different arrays with same contents are not equal
    return arr1 === arr2;
}

const x = [1, 2, 3];
const y = [1, 2, 3];
console.log(x === y);  // false!

Fixed Code

// SAFE: Use equals() for string comparison
public class SafeAuth {

    public boolean authenticate(String inputPassword, String storedPassword) {
        // Correct: Compares string content
        if (inputPassword == null || storedPassword == null) {
            return false;
        }
        return inputPassword.equals(storedPassword);
    }

    // Even safer: null-safe comparison
    public boolean authenticateSafe(String inputPassword, String storedPassword) {
        return Objects.equals(inputPassword, storedPassword);
    }

    // For security: constant-time comparison
    public boolean authenticateSecure(String inputPassword, String storedPassword) {
        if (inputPassword == null || storedPassword == null) {
            return false;
        }
        return MessageDigest.isEqual(
            inputPassword.getBytes(StandardCharsets.UTF_8),
            storedPassword.getBytes(StandardCharsets.UTF_8)
        );
    }
}

// SAFE: Use equals() for Integer comparison
public class SafeIdCheck {

    public boolean isSameUser(Integer userId1, Integer userId2) {
        // Correct: Works for all integer values
        return Objects.equals(userId1, userId2);
    }

    // Alternative with null handling
    public boolean isSameUserExplicit(Integer userId1, Integer userId2) {
        if (userId1 == null || userId2 == null) {
            return userId1 == userId2;  // Both null = equal
        }
        return userId1.equals(userId2);
    }
}

// SAFE: Custom object with proper equals() and hashCode()
public class User {
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User other = (User) obj;
        return Objects.equals(id, other.id) &&
               Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

public class SafeUserService {

    public boolean isSameUser(User user1, User user2) {
        // Now compares content!
        return Objects.equals(user1, user2);
    }

    public boolean containsUser(List<User> users, User target) {
        // Works correctly with proper equals()
        return users.contains(target);
    }
}

// SAFE: Token validation with proper comparison
public class SafeTokenValidator {

    public boolean validateToken(String providedToken, String expectedToken) {
        if (providedToken == null || expectedToken == null) {
            return false;
        }
        // Constant-time comparison for security tokens
        return MessageDigest.isEqual(
            providedToken.getBytes(StandardCharsets.UTF_8),
            expectedToken.getBytes(StandardCharsets.UTF_8)
        );
    }
}

// SAFE: Use real enum for type-safe comparison
public enum Status {
    ACTIVE,
    INACTIVE
}

public class SafeStatusCheck {

    public boolean isActive(Status status) {
        // Enum comparison with == is correct and preferred
        return status == Status.ACTIVE;
    }
}

// SAFE: Boolean comparison
public class SafeBooleanCheck {

    public boolean checkFlag(Boolean flag) {
        // Use equals() or compare to primitive
        return Boolean.TRUE.equals(flag);
    }

    // Or unbox safely
    public boolean checkFlagUnbox(Boolean flag) {
        return flag != null && flag.booleanValue();
    }
}

// SAFE: Array comparison
public class SafeArrayCheck {

    public boolean sameArray(byte[] arr1, byte[] arr2) {
        // Compares array contents
        return Arrays.equals(arr1, arr2);
    }

    // For security-sensitive byte arrays
    public boolean sameArraySecure(byte[] arr1, byte[] arr2) {
        return MessageDigest.isEqual(arr1, arr2);
    }
}

// SAFE: Date comparison
public class SafeDateCheck {

    public boolean isSameDate(Date date1, Date date2) {
        return Objects.equals(date1, date2);
    }

    // Or with null handling
    public boolean isSameDateExplicit(Date date1, Date date2) {
        if (date1 == null || date2 == null) {
            return date1 == date2;
        }
        return date1.equals(date2);
    }
}
// SAFE: JavaScript object comparison
function safeUserCheck(user1, user2) {
    // Compare by relevant properties
    if (user1 === null || user2 === null) {
        return user1 === user2;
    }
    return user1.id === user2.id && user1.name === user2.name;
}

// Generic deep equality check
function deepEquals(obj1, obj2) {
    if (obj1 === obj2) return true;
    if (obj1 === null || obj2 === null) return false;
    if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;

    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);

    if (keys1.length !== keys2.length) return false;

    return keys1.every(key => deepEquals(obj1[key], obj2[key]));
}

// SAFE: Array comparison
function safeArrayCheck(arr1, arr2) {
    if (arr1 === arr2) return true;
    if (!arr1 || !arr2) return false;
    if (arr1.length !== arr2.length) return false;

    return arr1.every((val, idx) => val === arr2[idx]);
}

// Or use lodash/underscore
// _.isEqual(arr1, arr2);

// SAFE: JSON comparison for simple objects
function jsonEquals(obj1, obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
}

Exploited in the Wild

Authentication Bypasses

String comparison bugs in password validation have allowed attackers to bypass authentication.

Session Fixation

Session ID comparison using == allowed session hijacking when IDs matched by value but not reference.

Authorization Failures

Role checking with reference comparison allowed privilege escalation.


Tools to test/exploit

  • SpotBugs — detects reference comparison issues.

  • SonarQube — identifies comparison bugs.

  • Error Prone — Google's static analysis tool.

  • PMD — source code analyzer.


CVE Examples

  • Authentication bypass CVEs from string comparison errors.

  • Session management vulnerabilities from reference comparison.


References

  1. MITRE. "CWE-595: Comparison of Object References Instead of Object Contents." https://cwe.mitre.org/data/definitions/595.html

  2. Effective Java, Item 10: Obey the general contract when overriding equals.