Verwendung falschen Operators beim String-Vergleich
Beschreibung
Verwendung falschen Operators beim String-Vergleich tritt auf, wenn Code Identitätsoperatoren (==, ===, is) anstelle von Gleichheitsmethoden verwendet, um Strings zu vergleichen. Während dies sich mit allgemeinen Objektvergleichsproblemen überschneidet, verdient String-Vergleich besondere Aufmerksamkeit, weil Strings fundamental für Sicherheit sind (Passwörter, Tokens, Benutzernamen) und weil String-Interning Fehler intermittierend erscheinen lassen kann.
Risiko
String-Vergleichsfehler in der Authentifizierung ermöglichen Passwort-Bypass. Token-Validierungsfehler ermöglichen Session-Hijacking. Benutzernamen-Vergleiche können fehlschlagen und Autorisierungsprobleme verursachen. String-Interning macht Fehler inkonsistent - Vergleich kann für kurze Strings oder Literale funktionieren, aber für dynamisch konstruierte Strings fehlschlagen. Diese Inkonsistenz macht Tests schwierig und Produktionsfehler unvorhersehbar.
Lösung
Verwenden Sie immer angemessene String-Vergleichsmethoden: equals() in Java, strcmp() in C, === mit Strings (nicht Objekten) in JavaScript, == in Python (das Wertvergleich für Strings macht). Seien Sie sich des String-Interning-Verhaltens bewusst. Für sicherheitssensitive Vergleiche verwenden Sie konstante Zeitvergleiche, um Timing-Angriffe zu verhindern.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Sicherheit | Bereich: Authentifizierungs-Bypass Passwort-/Token-Vergleich kann falsch erfolgreich sein oder fehlschlagen. |
| Logik | Bereich: Inkonsistentes Verhalten Kann in Tests funktionieren, aber in Produktion fehlschlagen. |
| Zuverlässigkeit | Bereich: Unvorhersehbare Fehler String-Interning erzeugt intermittierende Fehler. |
Beispielcode + Lösungscode
Verwundbarer Code
// VERWUNDBAR: String-Vergleich mit ==
public class VulnerableStringAuth {
private static final String ADMIN_PASSWORD = "admin123";
public boolean checkPassword(String input) {
// FEHLER: Referenzvergleich!
return input == ADMIN_PASSWORD;
}
public boolean validateApiKey(String providedKey, String expectedKey) {
// FEHLER: Kann selbst mit korrektem Schlüssel fehlschlagen!
return providedKey == expectedKey;
}
public void demonstrateInterning() {
String s1 = "hello"; // Interned
String s2 = "hello"; // Gleicher internierter String
String s3 = new String("hello"); // Neues Objekt
String s4 = "hel" + "lo"; // Compile-Zeit-Concat - interned
String s5 = "hel";
s5 += "lo"; // Laufzeit-Concat - NICHT interned
System.out.println(s1 == s2); // true (beide interned)
System.out.println(s1 == s3); // false (verschiedene Objekte)
System.out.println(s1 == s4); // true (Compile-Zeit-Interning)
System.out.println(s1 == s5); // false! (Laufzeit-Konstruktion)
}
}
// VERWUNDBAR: Benutzernamen-Vergleich
public class VulnerableUserLookup {
public User findUser(List<User> users, String username) {
for (User user : users) {
// FEHLER: Referenzvergleich kann fehlschlagen!
if (user.getUsername() == username) {
return user;
}
}
return null;
}
}
// VERWUNDBAR: String-Vergleich in Switch-Statement-Workaround
public class VulnerableRoleCheck {
public int getPermissionLevel(String role) {
// Vor Java 7 wurde Switch auf Strings nicht unterstützt
// Manche Entwickler verwendeten == als Workaround
if (role == "admin") {
return 100;
} else if (role == "user") {
return 10;
}
return 0;
}
}
// VERWUNDBAR: C String-Vergleich mit ==
#include <string.h>
int vulnerable_auth(const char* input, const char* password) {
// FEHLER: Vergleicht Pointer, nicht String-Inhalte!
if (input == password) {
return 1; // Authentifiziert
}
return 0;
}
void demonstrate_problem() {
char* s1 = "hello";
char* s2 = "hello";
char s3[] = "hello";
// s1 == s2 könnte true sein (String-Literal-Pooling)
// s1 == s3 ist false (s3 ist auf Stack)
printf("s1 == s2: %d\n", s1 == s2); // Könnte 1 sein
printf("s1 == s3: %d\n", s1 == s3); // 0
}
// VERWUNDBAR: Token-Validierung
int vulnerable_validate_token(const char* provided, const char* expected) {
return provided == expected; // FEHLER!
}
# VERWUNDBAR: Python String-Vergleich mit 'is'
def vulnerable_auth(input_password, stored_password):
# FEHLER: 'is' vergleicht Identität, nicht Wert!
return input_password is stored_password
def demonstrate_interning():
# Kurze Strings können interned sein
a = "hello"
b = "hello"
print(a is b) # True (interned)
# Längere oder dynamische Strings möglicherweise nicht
c = "hello world " * 100
d = "hello world " * 100
print(c is d) # Kann False sein!
# Benutzereingabe wird nie interned
e = input("Geben Sie 'hello' ein: ")
print(e is "hello") # False selbst wenn Benutzer "hello" eingibt
# VERWUNDBAR: Befehlsvergleich
def vulnerable_command_check(cmd):
if cmd is "quit": # FEHLER!
return True
return False
// JavaScript: Verwendung von == kann Typumwandlungsprobleme haben
function vulnerableCompare(input, expected) {
// == führt Typumwandlung durch, was Probleme verursachen kann
return input == expected;
}
// Beispiel problematischer Umwandlung
console.log("0" == false); // true!
console.log("" == false); // true!
console.log(null == undefined); // true!
// VERWUNDBAR: Objekt-String-Vergleich
function vulnerableObjectStringCompare(str1, str2) {
// Wenn einer ein String-Objekt ist (nicht Primitiv), schlägt dies fehl
return str1 === str2;
}
var a = new String("hello");
var b = new String("hello");
console.log(a === b); // false!
Lösungscode
// SICHER: equals() für String-Vergleich verwenden
public class SafeStringAuth {
private static final String ADMIN_PASSWORD = "admin123";
public boolean checkPassword(String input) {
// Korrekt: Vergleicht String-Inhalt
if (input == null) {
return false;
}
return ADMIN_PASSWORD.equals(input);
}
// Null-sichere Version
public boolean checkPasswordSafe(String input) {
return Objects.equals(input, ADMIN_PASSWORD);
}
// Konstante Zeitvergleich für Sicherheit
public boolean checkPasswordSecure(String input, String stored) {
if (input == null || stored == null) {
return false;
}
return MessageDigest.isEqual(
input.getBytes(StandardCharsets.UTF_8),
stored.getBytes(StandardCharsets.UTF_8)
);
}
// Groß-/Kleinschreibung ignorierender Vergleich
public boolean checkUsernameIgnoreCase(String input, String stored) {
if (input == null || stored == null) {
return false;
}
return input.equalsIgnoreCase(stored);
}
}
// SICHER: Benutzernamen-Suche mit ordnungsgemäßem Vergleich
public class SafeUserLookup {
public User findUser(List<User> users, String username) {
if (username == null) {
return null;
}
for (User user : users) {
if (username.equals(user.getUsername())) {
return user;
}
}
return null;
}
// Mit Streams
public Optional<User> findUserStream(List<User> users, String username) {
if (username == null) {
return Optional.empty();
}
return users.stream()
.filter(u -> username.equals(u.getUsername()))
.findFirst();
}
}
// SICHER: Modernes Java Switch-Statement
public class SafeRoleCheck {
public int getPermissionLevel(String role) {
if (role == null) {
return 0;
}
// Java 7+ unterstützt Switch auf Strings (verwendet intern equals)
switch (role) {
case "admin":
return 100;
case "user":
return 10;
default:
return 0;
}
}
}
// SICHER: strcmp() für C String-Vergleich verwenden
#include <string.h>
int safe_auth(const char* input, const char* password) {
if (input == NULL || password == NULL) {
return 0;
}
// Korrekt: Vergleicht String-Inhalte
if (strcmp(input, password) == 0) {
return 1;
}
return 0;
}
// SICHER: Groß-/Kleinschreibung ignorierender Vergleich
int safe_auth_nocase(const char* input, const char* password) {
if (input == NULL || password == NULL) {
return 0;
}
return strcasecmp(input, password) == 0;
}
// SICHER: Konstante Zeitvergleich für Sicherheit
int safe_auth_constant_time(const char* input, const char* password) {
if (input == NULL || password == NULL) {
return 0;
}
size_t input_len = strlen(input);
size_t pass_len = strlen(password);
// Konstante Zeitvergleich
volatile int result = input_len ^ pass_len;
size_t min_len = input_len < pass_len ? input_len : pass_len;
for (size_t i = 0; i < min_len; i++) {
result |= input[i] ^ password[i];
}
return result == 0;
}
// SICHER: Token-Validierung
int safe_validate_token(const char* provided, const char* expected) {
if (provided == NULL || expected == NULL) {
return 0;
}
return strcmp(provided, expected) == 0;
}
# SICHER: == für Python String-Vergleich verwenden
def safe_auth(input_password, stored_password):
# Korrekt: == vergleicht Werte in Python
if input_password is None or stored_password is None:
return False
return input_password == stored_password
# SICHER: Konstante Zeitvergleich für Sicherheit
import hmac
def safe_auth_secure(input_password, stored_password):
if input_password is None or stored_password is None:
return False
# Konstante Zeitvergleich
return hmac.compare_digest(input_password, stored_password)
# SICHER: Befehlsvergleich
def safe_command_check(cmd):
if cmd is None:
return False
return cmd == "quit" # == für Wertvergleich
# SICHER: Groß-/Kleinschreibung ignorierender Vergleich
def safe_username_check(input_name, stored_name):
if input_name is None or stored_name is None:
return False
return input_name.lower() == stored_name.lower()
// SICHER: JavaScript String-Vergleich
function safeCompare(input, expected) {
// === für strikte Gleichheit verwenden (keine Typumwandlung)
// Funktioniert korrekt für primitive Strings
return input === expected;
}
// SICHER: String-Objekte behandeln
function safeCompareAny(str1, str2) {
// Bei Bedarf zu Primitiven konvertieren
if (str1 === null || str2 === null) {
return str1 === str2;
}
return String(str1) === String(str2);
}
// SICHER: Konstante Zeitvergleich (Node.js)
const crypto = require('crypto');
function safeCompareSecure(input, expected) {
if (typeof input !== 'string' || typeof expected !== 'string') {
return false;
}
// Konstante Zeitvergleich
try {
return crypto.timingSafeEqual(
Buffer.from(input),
Buffer.from(expected)
);
} catch (e) {
return false; // Verschiedene Längen
}
}
Ausgenutzt in der Praxis
Authentifizierungs-Bypasses
Passwort-Vergleichsfehler mit == ermöglichten Angreifern, Authentifizierungssysteme zu umgehen.
API-Schlüssel-Validierung
API-Schlüssel-Validierungsfehler durch Referenzvergleich ermöglichten unbefugten Zugriff.
Session-Token-Probleme
Session-Tokens, die mit Identitätsoperatoren verglichen wurden, verursachten Session-Management-Schwachstellen.
Tools zum Testen/Ausnutzen
- SpotBugs — ES_COMPARING_STRINGS_WITH_EQ Detektor
- SonarQube — String-Vergleichsregeln
- Pylint — Python 'is' mit Literal-Erkennung
- ESLint — eqeqeq-Regel
CVE-Beispiele
- CVEs durch String-Vergleichsfehler in der Authentifizierung
- Token-Validierungs-Bypasses durch Identitätsvergleich
Referenzen
- MITRE. "CWE-597: Use of Wrong Operator in String Comparison." https://cwe.mitre.org/data/definitions/597.html
- Java Language Specification - String Interning.