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
| Auswirkung | Details |
|---|---|
| Sicherheit | Bereich: Umgangene Prüfungen Sicherheitsvalidierungen, die nie auslösen, bieten keinen Schutz. |
| Zuverlässigkeit | Bereich: Unbehandelte Fehler Fehlerbehandlung, die nie ausgeführt wird, lässt Fehler unbehandelt. |
| Logik | Bereich: 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
- MITRE. "CWE-570: Expression is Always False." https://cwe.mitre.org/data/definitions/570.html
- 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/