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

AuswirkungDetails
SicherheitBereich: Authentifizierungs-Bypass

Passwort-/Token-Vergleich kann falsch erfolgreich sein oder fehlschlagen.
LogikBereich: Inkonsistentes Verhalten

Kann in Tests funktionieren, aber in Produktion fehlschlagen.
ZuverlässigkeitBereich: 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

  1. MITRE. "CWE-597: Use of Wrong Operator in String Comparison." https://cwe.mitre.org/data/definitions/597.html
  2. Java Language Specification - String Interning.