Vergleich von Objektreferenzen statt Objektinhalten

Beschreibung

Vergleich von Objektreferenzen statt Objektinhalten tritt auf, wenn Code Referenzgleichheitsoperatoren (== oder !=) verwendet, um Objekte zu vergleichen, wenn er stattdessen ihre Werte mit equals() oder ähnlichen Methoden vergleichen sollte. In Sprachen wie Java vergleicht == Speicheradressen (ob zwei Referenzen auf dasselbe Objekt zeigen), nicht den tatsächlichen Inhalt von Objekten. Dies führt zu unerwartetem Verhalten, wenn logisch gleiche Objekte als unterschiedlich betrachtet werden.

Risiko

Referenzvergleichsfehler verursachen Authentifizierungs-Bypasses, wenn String-Passwörter falsch verglichen werden. Geschäftslogik schlägt fehl, wenn äquivalente Objekte nicht als gleich erkannt werden. Sicherheitstoken können nicht ordnungsgemäß validiert werden. Caching schlägt fehl, wenn gleiche Objekte nicht erkannt werden. Collections verhalten sich unerwartet. Der Code kann mit manchen Eingaben funktionieren (aufgrund von String-Interning oder Boxing-Caches), aber mit anderen fehlschlagen.

Lösung

Verwenden Sie die equals()-Methode für Objektvergleiche in Java. Verwenden Sie Objects.equals() für null-sichere Vergleiche. Überschreiben Sie equals() und hashCode(), wenn Sie Wertklassen erstellen. In JavaScript verstehen Sie den Unterschied zwischen == und === für Objekte. Seien Sie sich der sprachspezifischen Vergleichssemantik bewusst. Verwenden Sie angemessene Vergleichsmethoden für Strings, Zahlen und benutzerdefinierte Objekte.

Häufige Auswirkungen

AuswirkungDetails
LogikBereich: Falsches Verhalten

Logisch gleiche Objekte werden als unterschiedlich behandelt.
SicherheitBereich: Authentifizierungs-Bypass

Credential-Vergleich kann falsch erfolgreich sein oder fehlschlagen.
ZuverlässigkeitBereich: Inkonsistente Ergebnisse

Code kann manchmal aufgrund von Caching/Interning funktionieren.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: String-Vergleich mit ==
public class VulnerableAuth {

    public boolean authenticate(String inputPassword, String storedPassword) {
        // FEHLER: Vergleicht Referenzen, nicht Inhalt!
        return inputPassword == storedPassword;
    }

    public boolean checkRole(String userRole, String requiredRole) {
        // Kann manchmal aufgrund von String-Interning funktionieren, manchmal fehlschlagen
        return userRole == requiredRole;
    }
}

// VERWUNDBAR: Integer-Vergleich mit ==
public class VulnerableIdCheck {

    public boolean isSameUser(Integer userId1, Integer userId2) {
        // FEHLER: Funktioniert für -128 bis 127 (gecacht), schlägt für größere Werte fehl!
        return userId1 == userId2;
    }

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

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

// VERWUNDBAR: Benutzerdefinierter Objektvergleich
public class User {
    private Long id;
    private String name;

    // Kein equals()-Override - verwendet Object.equals (Referenzvergleich)
}

public class VulnerableUserService {

    public boolean isSameUser(User user1, User user2) {
        // FEHLER: Vergleicht Referenzen, nicht Inhalt!
        return user1 == user2;
    }

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

// VERWUNDBAR: Token-Validierung
public class VulnerableTokenValidator {

    public boolean validateToken(String providedToken, String expectedToken) {
        // FEHLER: Kann selbst mit korrektem Token fehlschlagen!
        return providedToken == expectedToken;
    }
}

// VERWUNDBAR: Enum-ähnlicher Klassenvergleich
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) {
        // FEHLER: new Status("ACTIVE") == Status.ACTIVE ist false!
        return status == Status.ACTIVE;
    }
}
// VERWUNDBAR: Boolean-Vergleich
public class VulnerableBooleanCheck {

    public boolean checkFlag(Boolean flag) {
        // FEHLER: Kann für Boolean-Objekte fehlschlagen!
        return flag == Boolean.TRUE;
    }

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

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

// VERWUNDBAR: Array-Vergleich
public class VulnerableArrayCheck {

    public boolean sameArray(byte[] arr1, byte[] arr2) {
        // FEHLER: Vergleicht Referenzen, nicht Array-Inhalte!
        return arr1 == arr2;
    }
}

// VERWUNDBAR: Datum-Vergleich
public class VulnerableDateCheck {

    public boolean isSameDate(Date date1, Date date2) {
        // FEHLER: Referenzvergleich!
        return date1 == date2;
    }
}
// VERWUNDBAR: JavaScript Objektvergleich
function vulnerableUserCheck(user1, user2) {
    // FEHLER: Objekte mit gleichem Inhalt sind nicht ===
    return user1 === user2;
}

// Demonstriert das Problem
const a = { name: "Alice" };
const b = { name: "Alice" };
console.log(a === b);  // false!

// VERWUNDBAR: Array-Vergleich
function vulnerableArrayCheck(arr1, arr2) {
    // FEHLER: Verschiedene Arrays mit gleichem Inhalt sind nicht gleich
    return arr1 === arr2;
}

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

Lösungscode

// SICHER: equals() für String-Vergleich verwenden
public class SafeAuth {

    public boolean authenticate(String inputPassword, String storedPassword) {
        // Korrekt: Vergleicht String-Inhalt
        if (inputPassword == null || storedPassword == null) {
            return false;
        }
        return inputPassword.equals(storedPassword);
    }

    // Noch sicherer: null-sicherer Vergleich
    public boolean authenticateSafe(String inputPassword, String storedPassword) {
        return Objects.equals(inputPassword, storedPassword);
    }

    // Für Sicherheit: konstante Zeitvergleich
    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)
        );
    }
}

// SICHER: equals() für Integer-Vergleich verwenden
public class SafeIdCheck {

    public boolean isSameUser(Integer userId1, Integer userId2) {
        // Korrekt: Funktioniert für alle Integer-Werte
        return Objects.equals(userId1, userId2);
    }

    // Alternative mit null-Behandlung
    public boolean isSameUserExplicit(Integer userId1, Integer userId2) {
        if (userId1 == null || userId2 == null) {
            return userId1 == userId2;  // Beide null = gleich
        }
        return userId1.equals(userId2);
    }
}

// SICHER: Benutzerdefiniertes Objekt mit ordnungsgemäßem equals() und 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) {
        // Vergleicht jetzt Inhalt!
        return Objects.equals(user1, user2);
    }

    public boolean containsUser(List<User> users, User target) {
        // Funktioniert korrekt mit ordnungsgemäßem equals()
        return users.contains(target);
    }
}

// SICHER: Token-Validierung mit ordnungsgemäßem Vergleich
public class SafeTokenValidator {

    public boolean validateToken(String providedToken, String expectedToken) {
        if (providedToken == null || expectedToken == null) {
            return false;
        }
        // Konstante Zeitvergleich für Sicherheitstokens
        return MessageDigest.isEqual(
            providedToken.getBytes(StandardCharsets.UTF_8),
            expectedToken.getBytes(StandardCharsets.UTF_8)
        );
    }
}

// SICHER: Echtes enum für typsicheren Vergleich verwenden
public enum Status {
    ACTIVE,
    INACTIVE
}

public class SafeStatusCheck {

    public boolean isActive(Status status) {
        // Enum-Vergleich mit == ist korrekt und bevorzugt
        return status == Status.ACTIVE;
    }
}

// SICHER: Boolean-Vergleich
public class SafeBooleanCheck {

    public boolean checkFlag(Boolean flag) {
        // equals() verwenden oder mit Primitiv vergleichen
        return Boolean.TRUE.equals(flag);
    }

    // Oder sicher unboxen
    public boolean checkFlagUnbox(Boolean flag) {
        return flag != null && flag.booleanValue();
    }
}

// SICHER: Array-Vergleich
public class SafeArrayCheck {

    public boolean sameArray(byte[] arr1, byte[] arr2) {
        // Vergleicht Array-Inhalte
        return Arrays.equals(arr1, arr2);
    }

    // Für sicherheitssensitive Byte-Arrays
    public boolean sameArraySecure(byte[] arr1, byte[] arr2) {
        return MessageDigest.isEqual(arr1, arr2);
    }
}

// SICHER: Datum-Vergleich
public class SafeDateCheck {

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

    // Oder mit null-Behandlung
    public boolean isSameDateExplicit(Date date1, Date date2) {
        if (date1 == null || date2 == null) {
            return date1 == date2;
        }
        return date1.equals(date2);
    }
}
// SICHER: JavaScript Objektvergleich
function safeUserCheck(user1, user2) {
    // Nach relevanten Eigenschaften vergleichen
    if (user1 === null || user2 === null) {
        return user1 === user2;
    }
    return user1.id === user2.id && user1.name === user2.name;
}

// Generischer 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]));
}

// SICHER: Array-Vergleich
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]);
}

// Oder lodash/underscore verwenden
// _.isEqual(arr1, arr2);

// SICHER: JSON-Vergleich für einfache Objekte
function jsonEquals(obj1, obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
}

Ausgenutzt in der Praxis

Authentifizierungs-Bypasses

String-Vergleichsfehler in Passwortvalidierung haben Angreifern ermöglicht, Authentifizierung zu umgehen.

Session-Fixierung

Session-ID-Vergleich mit == ermöglichte Session-Hijacking, wenn IDs nach Wert aber nicht nach Referenz übereinstimmten.

Autorisierungsfehler

Rollenprüfung mit Referenzvergleich ermöglichte Privilege-Escalation.


Tools zum Testen/Ausnutzen

  • SpotBugs — erkennt Referenzvergleichsprobleme
  • SonarQube — identifiziert Vergleichsfehler
  • Error Prone — Googles statisches Analysetool
  • PMD — Quellcode-Analysator

CVE-Beispiele

  • Authentifizierungs-Bypass-CVEs durch String-Vergleichsfehler
  • Session-Management-Schwachstellen durch Referenzvergleich

Referenzen

  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.