Ausdruck ist immer falsch

Beschreibung

Ausdruck ist immer falsch tritt auf, wenn ein bedingter Ausdruck unter allen möglichen Umständen zu falsch auswertet, aufgrund logischer Widersprüche, Typeinschränkungen oder konstanter Werte. Häufige Muster umfassen Vergleiche von unsigned-Werten mit negativen Zahlen (immer falsch), widersprüchliche zusammengesetzte Bedingungen, Vergleich einer Variablen mit einem Wert außerhalb ihres möglichen Bereichs und redundante Prüfungen nach früheren validierenden Bedingungen. Der durch die Bedingung geschützte Code wird nie ausgeführt, was ihn effektiv zu totem Code macht.

Risiko

Immer-falsche Ausdrücke weisen auf Logikfehler hin, die oft Sicherheitsimplikationen haben. Sicherheitsprüfungen, die nie auslösen, bieten keinen Schutz. Fehlerbehandlungscode, der nie ausgeführt wird, lässt Fehler unbehandelt. Bounds-Checking, das nie fehlschlägt, erlaubt Pufferüberläufe. Die Absicht des Programmierers wird nicht realisiert, und das resultierende Verhalten unterscheidet sich von der erwarteten sicheren Operation. Angreifer können die Lücke zwischen angenommenem und tatsächlichem Programmverhalten ausnutzen.

Lösung

Aktivieren Sie Compiler-Warnungen für tautologische Vergleiche und unmögliche Bedingungen. Verwenden Sie statische Analysewerkzeuge, die immer-falsche Ausdrücke erkennen. Verstehen Sie Typeinschränkungen - unsigned-Werte können nicht negativ sein. Überprüfen Sie zusammengesetzte Bedingungen auf Widersprüche. Verfolgen Sie Wertebereiche durch Codepfade. Verifizieren Sie, dass Defensivprüfungen tatsächlich auslösen können. Testen Sie Randbedingungen, um sicherzustellen, dass Prüfungen aktiviert werden, wenn sie sollten.

Häufige Auswirkungen

AuswirkungDetails
SicherheitBereich: Umgangene Prüfungen

Sicherheitsvalidierungen, die nie auslösen, bieten keinen Schutz.
ZuverlässigkeitBereich: Unbehandelte Fehler

Fehlerbehandlung, die nie ausgeführt wird, lässt Fehler unbehandelt.
LogikBereich: Toter Code

Codepfade, die nie genommen werden können, dienen keinem Zweck.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Unsigned-Vergleich mit negativ (immer falsch)
void check_size_vulnerable(size_t size) {
    if (size < 0) {  // IMMER FALSCH! size_t ist unsigned!
        handle_error("Ungültige Größe");
        return;
    }

    // Fährt fort auch mit "ungültigen" Größen
    process(size);
}

// VERWUNDBAR: Unsigned-Unterlauf nicht abgefangen
void process_buffer_vulnerable(unsigned int length) {
    if (length < 0) {  // IMMER FALSCH!
        return;
    }

    // length könnte UINT_MAX sein (durch Unterlauf)
    char* buffer = malloc(length);
    // Potenzielle Probleme mit riesiger Allokation
}

// VERWUNDBAR: Widersprüchliche Bedingungen
void validate_vulnerable(int value) {
    if (value > 100 && value < 50) {  // IMMER FALSCH!
        // Unmögliche Bedingung
        handle_special_case();  // Wird nie ausgeführt!
    }
}

// VERWUNDBAR: Bereich bereits geprüft
void check_range_vulnerable(int value) {
    if (value < 0 || value > 100) {
        return;  // Ungültiger Bereich abgelehnt
    }

    // Hier ist value in [0, 100]
    if (value < 0) {  // IMMER FALSCH! Bereits oben geprüft!
        handle_negative();  // Toter Code!
    }

    process(value);
}

// VERWUNDBAR: Enum-Wert-Vergleich
typedef enum { NONE = 0, LOW = 1, HIGH = 2 } Priority;

void handle_priority_vulnerable(Priority p) {
    if (p == NONE) {
        return;
    }

    // p ist jetzt LOW oder HIGH
    if (p == NONE) {  // IMMER FALSCH!
        // Toter Code
        handle_none();
    }
}

// VERWUNDBAR: Pointer nach NULL-Prüfung
void use_pointer_vulnerable(int* ptr) {
    if (ptr == NULL) {
        return;
    }

    // ptr ist hier nicht NULL
    if (ptr == NULL) {  // IMMER FALSCH!
        log_error("NULL-Pointer");  // Protokolliert nie!
    }

    *ptr = 42;
}

// VERWUNDBAR: Boolean-Logikfehler
void check_flags_vulnerable(int flag1, int flag2) {
    if (!flag1) {
        return;  // flag1 ist falsch, zurückkehren
    }

    // flag1 ist hier wahr
    if (!flag1 && flag2) {  // IMMER FALSCH! flag1 ist wahr!
        // Toter Code
        handle_case();
    }
}

// VERWUNDBAR: Char-Vergleich auf einigen Plattformen
void check_char_vulnerable(char c) {
    // Auf Plattformen wo char unsigned ist:
    if (c < 0) {  // IMMER FALSCH bei unsigned char!
        handle_negative_char();
    }
}
// VERWUNDBAR: C++ mit immer-falschen Bedingungen
void checkValue_vulnerable(unsigned int value) {
    if (value < 0) {  // IMMER FALSCH!
        throw std::invalid_argument("Negativer Wert");
    }
    // Exception wird nie geworfen
}

// VERWUNDBAR: String-Vergleich
void checkString_vulnerable(const std::string& str) {
    if (str.length() < 0) {  // IMMER FALSCH! length() gibt size_t zurück
        handleError();
    }
}

// VERWUNDBAR: Vector-Größenprüfung
void processVector_vulnerable(const std::vector<int>& vec) {
    if (vec.size() < 0) {  // IMMER FALSCH!
        return;
    }

    // Fährt immer fort
    for (int i : vec) {
        process(i);
    }
}

// VERWUNDBAR: Smart-Pointer nach Prüfung
void usePointer_vulnerable(std::shared_ptr<Resource> ptr) {
    if (!ptr) {
        return;
    }

    // ptr ist gültig
    if (!ptr) {  // IMMER FALSCH!
        log("Null Pointer");  // Toter Code
    }

    ptr->use();
}

// VERWUNDBAR: Optional nach Prüfung
void useOptional_vulnerable(std::optional<int> opt) {
    if (!opt) {
        return;
    }

    // opt hat Wert
    if (!opt.has_value()) {  // IMMER FALSCH!
        handleEmpty();  // Toter Code
    }

    process(*opt);
}
// VERWUNDBAR: JavaScript immer-falsche Bedingungen
function checkVulnerable(length) {
    if (length < 0 && length > 100) {  // IMMER FALSCH!
        throw new Error('Ungültige Länge');
    }
}

// VERWUNDBAR: Typkonversionsprobleme
function compareVulnerable(value) {
    if (value === 1 && value === 2) {  // IMMER FALSCH!
        // Toter Code
        handleBoth();
    }
}

// VERWUNDBAR: Nach Null-Prüfung
function useObjectVulnerable(obj) {
    if (obj === null) {
        return;
    }

    // obj ist nicht null
    if (obj === null) {  // IMMER FALSCH!
        console.log('Null!');
    }

    obj.doSomething();
}

// VERWUNDBAR: Array-Längenprüfung
function processArrayVulnerable(arr) {
    if (arr.length < 0) {  // IMMER FALSCH! length >= 0
        return;
    }

    // Unerreichbares return
}

Lösungscode

// SICHER: Signed-Typ für Werte verwenden, die negativ sein können
void check_size_safe(ssize_t size) {  // ssize_t ist signed
    if (size < 0) {  // Jetzt kann dies wahr sein
        handle_error("Ungültige Größe");
        return;
    }

    process((size_t)size);  // Nach Validierung konvertieren
}

// SICHER: Auf Null oder Überlauf-indikative Werte prüfen
void process_buffer_safe(unsigned int length) {
    if (length == 0 || length > MAX_REASONABLE_SIZE) {
        return;
    }

    char* buffer = malloc(length);
    if (buffer) {
        process(buffer, length);
        free(buffer);
    }
}

// SICHER: Widersprüchliche Bedingung beheben
void validate_safe(int value) {
    if (value > 50 && value < 100) {  // Gültiger Bereich
        handle_special_case();
    }
}

// SICHER: Redundante Prüfung entfernen
void check_range_safe(int value) {
    if (value < 0 || value > 100) {
        return;
    }

    // Keine redundante Prüfung nötig
    process(value);
}

// SICHER: Ordnungsgemäße Enum-Behandlung
void handle_priority_safe(Priority p) {
    switch (p) {
        case NONE:
            handle_none();
            break;
        case LOW:
            handle_low();
            break;
        case HIGH:
            handle_high();
            break;
    }
}

// SICHER: Keine redundante NULL-Prüfung
void use_pointer_safe(int* ptr) {
    if (ptr == NULL) {
        log_error("NULL-Pointer");
        return;
    }

    // Nicht nötig nochmal zu prüfen
    *ptr = 42;
}

// SICHER: Korrekte Boolean-Logik
void check_flags_safe(int flag1, int flag2) {
    if (!flag1) {
        return;
    }

    // flag1 ist wahr
    if (flag2) {  // Nur flag2 prüfen
        handle_case();
    }
}

// SICHER: Explizit signed char verwenden wenn nötig
void check_char_safe(signed char c) {
    if (c < 0) {  // Jetzt kann wahr sein
        handle_negative_char();
    }
}

// SICHER: Klare Absicht mit Assertions
void process_with_assertions(size_t size) {
    // Annahme dokumentieren statt Fake-Prüfung
    assert(size > 0 && "Größe muss positiv sein");

    // Oder explizites früh-return für Null
    if (size == 0) {
        return;
    }

    process(size);
}
// SICHER: Geeigneten signed-Typ verwenden
void checkValue_safe(int value) {  // Signed-Typ
    if (value < 0) {  // Kann jetzt wahr sein
        throw std::invalid_argument("Negativer Wert");
    }
}

// SICHER: Auf leer statt negative Länge prüfen
void checkString_safe(const std::string& str) {
    if (str.empty()) {  // Sinnvolle Prüfung
        handleEmptyString();
    }
}

// SICHER: Ordnungsgemäße Vector-Behandlung
void processVector_safe(const std::vector<int>& vec) {
    if (vec.empty()) {  // Auf leer prüfen, nicht negative Größe
        return;
    }

    for (int i : vec) {
        process(i);
    }
}

// SICHER: Keine redundante Smart-Pointer-Prüfung
void usePointer_safe(std::shared_ptr<Resource> ptr) {
    if (!ptr) {
        log("Null Pointer");
        return;
    }

    ptr->use();  // Nach Prüfung bekannt gültig
}

// SICHER: Ordnungsgemäße Optional-Behandlung
void useOptional_safe(std::optional<int> opt) {
    if (!opt) {
        handleEmpty();
        return;
    }

    process(*opt);  // Bekannt Wert zu haben
}

// SICHER: static_assert für Compile-Zeit-Garantien verwenden
template<typename T>
void processContainer(const T& container) {
    // size_type ist immer unsigned, dies dokumentieren
    static_assert(std::is_unsigned_v<typename T::size_type>,
                 "Container-Größentyp muss unsigned sein");

    if (container.empty()) {
        return;
    }

    // Nicht-leeren Container verarbeiten
}
// SICHER: Korrekte Bereichsprüfung
function checkSafe(length) {
    if (length < 0 || length > 100) {  // Ordnungsgemäße ODER-Bedingung
        throw new Error('Ungültige Länge');
    }
}

// SICHER: Sinnvolle Prüfungen
function processSafe(value) {
    if (typeof value !== 'number' || isNaN(value)) {
        throw new Error('Ungültige Zahl');
    }

    if (value < 0) {
        throw new Error('Negativ nicht erlaubt');
    }
}

// SICHER: Keine redundante Null-Prüfung
function useObjectSafe(obj) {
    if (obj === null || obj === undefined) {
        console.log('Ungültiges Objekt');
        return;
    }

    obj.doSomething();  // Bekannt gültig
}

// SICHER: Leeres Array prüfen
function processArraySafe(arr) {
    if (!arr || arr.length === 0) {  // Auf leer prüfen, nicht negativ
        return;
    }

    for (let item of arr) {
        process(item);
    }
}

// ESLint kann einiges davon abfangen mit:
// - "no-constant-condition"
// - "@typescript-eslint/no-unnecessary-condition" (TypeScript)

Ausgenutzt in der Praxis

Bounds-Check-Umgehungen

Sicherheits-Bounds-Checks, die immer zu falsch auswerteten, wurden ausgenutzt, um Pufferüberläufe auszulösen.

Authentifizierungsumgehungen

Authentifizierungsprüfungen, die aufgrund von Typproblemen nie fehlschlagen könnten, haben nicht autorisierten Zugriff ermöglicht.

Eingabevalidierungsfehler

Eingabevalidierung, die aufgrund immer-falscher Bedingungen nie ungültige Eingaben ablehnte, hat Injection-Angriffe ermöglicht.


Tools zum Testen

  • GCC/Clang — -Wtype-limits, -Wtautological-compare.
  • Coverity — erkennt immer-falsche Bedingungen.
  • PVS-Studio — fängt unmögliche Vergleiche ab.
  • ESLint — no-constant-condition Regel.

CVE-Beispiele

  • Mehrere CVEs mit unsigned-Vergleichsbugs in Sicherheitsprüfungen.
  • Buffer Overflow-CVEs wo Bounds-Checks nie auslösten.
  • Eingabevalidierungsumgehungen durch Typfehlanpassung in Vergleichen.

Referenzen

  1. MITRE. "CWE-570: Expression is Always False." https://cwe.mitre.org/data/definitions/570.html
  2. CERT C. "INT31-C: Ensure that integer conversions do not result in löst or misinterpreted data." https://wiki.sei.cmu.edu/confluence/display/c/