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
| Auswirkung | Details |
|---|---|
| Logik | Bereich: Falsches Verhalten Logisch gleiche Objekte werden als unterschiedlich behandelt. |
| Sicherheit | Bereich: Authentifizierungs-Bypass Credential-Vergleich kann falsch erfolgreich sein oder fehlschlagen. |
| Zuverlässigkeit | Bereich: 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
- MITRE. "CWE-595: Comparison of Object References Instead of Object Contents." https://cwe.mitre.org/data/definitions/595.html
- Effective Java, Item 10: Obey the general contract when overriding equals.