Unerwarteter Statuscode oder Rückgabewert

Beschreibung

Unerwarteter Statuscode oder Rückgabewert ist eine Schwachstelle, die auftritt, wenn Software nicht ausreichend überprüft, ob eine Funktion oder Operation einen legitimen Wert zurückgibt, den das Produkt nicht zu empfangen erwartet hatte. Obwohl verwandt mit ungeprüften Rückgabewerten (CWE-252), betrifft diese Schwachstelle speziell Fälle, in denen der Code einige Rückgabewerte prüft, aber unerwartete, aber gültige Werte nicht behandelt. Das Programm kann von einer begrenzten Menge möglicher Rückgaben ausgehen, während die API tatsächlich zusätzliche legitime Statuscodes zurückgeben kann.

Risiko

Das Nichtbehandeln unerwarteter, aber gültiger Rückgabewerte kann dazu führen, dass Programme in undefinierte Zustände geraten. Netzwerkfunktionen wie recv() können weniger Bytes als erwartet oder spezielle Werte zurückgeben, die verschiedene Bedingungen anzeigen. DNS-Lookups können unerwartete Ergebnisse zurückgeben, die Zugriffskontrollen umgehen. Systemaufrufe können sich auf verschiedenen Plattformen oder Hardware unterschiedlich verhalten und Werte zurückgeben, die vom Code nicht behandelt werden. Wenn diese unerwarteten Werte nicht behandelt werden, können Programme in Endlosschleifen geraten, Speicher leaken, Daten uninitialisiert lassen oder Sicherheitsprüfungen umgehen. Die Schwachstelle ist besonders gefährlich, weil der Code den Anschein erweckt, Rückgabewerte zu prüfen, aber Grenzfälle übersieht.

Lösung

Überprüfen Sie die API-Dokumentation gründlich, um alle möglichen Rückgabewerte zu verstehen, nicht nur die häufigen. Behandeln Sie alle dokumentierten Rückgabewerte explizit, einschließlich Grenzfälle und plattformspezifisches Verhalten. Verwenden Sie defensive Programmiertechniken, die unerwartete Werte als Fehler behandeln, anstatt Erfolg anzunehmen. Implementieren Sie ordnungsgemäße Bereichsprüfungen, wenn Funktionen Zähler oder Größen zurückgeben. Testen Sie mit Fuzzing und Grenzfalltests, um Codepfade zu entdecken, die ungewöhnliche Rückgaben nicht behandeln. Erwägen Sie die Verwendung von Wrapper-Funktionen, die Rückgabewerte plattformübergreifend normalisieren.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Integrität, Sonstiges

Unerwarteter Zustand, veränderte Ausführungslogik -- Das Programm kann in unbeabsichtigte Zustände geraten oder unbeabsichtigte Logik ausführen, wenn unerwartete Werte empfangen werden.

Beispielcode und Lösung

Verwundbarer Code

// VERWUNDBAR: Behandelt nicht alle recv()-Rückgabewerte
void vulnerable_receive_data(int sock, char *buffer, size_t expected) {
    ssize_t received = recv(sock, buffer, expected, 0);

    // VERWUNDBAR: Prüft nur auf Fehler (-1)
    if (received == -1) {
        perror("recv failed");
        return;
    }

    // VERWUNDBAR: Behandelt nicht 0 (Verbindung geschlossen)
    // VERWUNDBAR: Behandelt kein partielles Lesen (received < expected)

    // Fährt fort, als ob alle Daten empfangen wurden
    process_data(buffer, expected);  // Kann uninitialisierten Speicher verwenden!
}

// VERWUNDBAR: Behandelt nicht unerwartete read()-Rückgaben
void vulnerable_read_file(int fd, char *buffer, size_t size) {
    ssize_t bytes_read = read(fd, buffer, size);

    if (bytes_read > 0) {
        // VERWUNDBAR: Nimmt an, alle Bytes wurden gelesen
        buffer[size - 1] = '\0';  // Kann Daten uninitialisiert lassen
    }
}

// VERWUNDBAR: Unerwarteter recvfrom-Rückgabewert bewirkt Schleifenabbruch
void vulnerable_server_loop(int sock) {
    char buffer[1024];
    struct sockaddr_in client;
    socklen_t client_len = sizeof(client);

    while (1) {
        ssize_t received = recvfrom(sock, buffer, sizeof(buffer), 0,
                                     (struct sockaddr*)&client, &client_len);

        // VERWUNDBAR: Null-Länge-Paket verursacht unerwartetes Verhalten
        if (received <= 0) {
            // Behandelt 0 wie Fehler, kann Schleife unerwartet beenden
            break;
        }

        handle_request(buffer, received, &client);
    }
}
# VERWUNDBAR: Unerwartetes DNS-Lookup-Ergebnis
def vulnerable_access_check(hostname):
    try:
        ip = socket.gethostbyname(hostname)
    except socket.gaierror:
        # Blockiert - Hostname wird nicht aufgelöst
        return False

    # VERWUNDBAR: Behandelt nicht leeres Ergebnis oder unerwartetes Format
    if ip is None:
        return False

    # VERWUNDBAR: Reverse-Lookup kann None zurückgeben
    reverse = socket.gethostbyaddr(ip)[0]

    # VERWUNDBAR: Wenn Reverse-Lookup fehlschlägt/unerwarteten Wert zurückgibt,
    # wird diese Prüfung umgangen
    if reverse != expected_hostname:
        return False

    return True

# VERWUNDBAR: Partielles Lesen nicht behandelt
def vulnerable_read_message(sock):
    # Erwartet genau 4 Bytes für die Nachrichtenlänge
    length_data = sock.recv(4)

    # VERWUNDBAR: recv() kann weniger als 4 Bytes zurückgeben
    length = struct.unpack('!I', length_data)[0]  # Kann fehlschlagen oder falschen Wert liefern!

    # Nachricht lesen
    message = sock.recv(length)  # Kann ebenfalls partiell sein!
    return message
// VERWUNDBAR: InputStream.read() partielles Lesen
public class VulnerableReader {

    public byte[] readExact(InputStream in, int size) throws IOException {
        byte[] buffer = new byte[size];

        // VERWUNDBAR: read() kann weniger Bytes als angefordert zurückgeben
        int bytesRead = in.read(buffer);

        if (bytesRead == -1) {
            throw new EOFException("Unexpected end of stream");
        }

        // VERWUNDBAR: Nimmt an, alle Bytes wurden gelesen
        return buffer;  // Kann uninitialisierte Daten enthalten!
    }
}

Sichere Lösung

// SICHER: Behandelt alle recv()-Rückgabewerte
int secure_receive_data(int sock, char *buffer, size_t expected) {
    size_t total_received = 0;

    while (total_received < expected) {
        ssize_t received = recv(sock, buffer + total_received,
                                expected - total_received, 0);

        // SICHER: Fehler behandeln
        if (received == -1) {
            perror("recv failed");
            return -1;
        }

        // SICHER: Geschlossene Verbindung behandeln
        if (received == 0) {
            fprintf(stderr, "Connection closed by peer\n");
            return -2;
        }

        // SICHER: Partielles Lesen durch Schleife behandeln
        total_received += received;
    }

    process_data(buffer, expected);
    return 0;
}

// SICHER: Behandelt alle read()-Szenarien
int secure_read_file(int fd, char *buffer, size_t size) {
    size_t total_read = 0;

    while (total_read < size) {
        ssize_t bytes_read = read(fd, buffer + total_read, size - total_read);

        if (bytes_read == -1) {
            if (errno == EINTR) {
                continue;  // Unterbrochen, erneut versuchen
            }
            return -1;  // Tatsächlicher Fehler
        }

        if (bytes_read == 0) {
            // EOF erreicht vor erwarteten Bytes
            return -2;
        }

        total_read += bytes_read;
    }

    return 0;  // Erfolg
}

// SICHER: Behandelt Null-Länge-Pakete korrekt
void secure_server_loop(int sock) {
    char buffer[1024];
    struct sockaddr_in client;
    socklen_t client_len;

    while (1) {
        client_len = sizeof(client);  // Jedes Mal zurücksetzen
        ssize_t received = recvfrom(sock, buffer, sizeof(buffer), 0,
                                     (struct sockaddr*)&client, &client_len);

        if (received == -1) {
            if (errno == EINTR) {
                continue;  // Unterbrochen, erneut versuchen
            }
            perror("recvfrom failed");
            break;  // Tatsächlicher Fehler
        }

        // SICHER: Null-Länge-Paket ist gültig, einfach Verarbeitung überspringen
        if (received == 0) {
            continue;
        }

        handle_request(buffer, received, &client);
    }
}
# SICHER: Behandelt alle DNS-Lookup-Ergebnisse
def secure_access_check(hostname):
    try:
        ip = socket.gethostbyname(hostname)
    except socket.gaierror as e:
        logger.warning(f"DNS lookup failed for {hostname}: {e}")
        return False

    if not ip:
        return False

    try:
        reverse_result = socket.gethostbyaddr(ip)
        reverse = reverse_result[0] if reverse_result else None
    except socket.herror:
        # SICHER: Behandelt Reverse-Lookup-Fehler als Zugriff verweigert
        logger.warning(f"Reverse DNS failed for {ip}")
        return False

    if reverse is None or reverse != expected_hostname:
        return False

    return True

# SICHER: Behandelt partielles Lesen
def secure_read_message(sock):
    # Genau 4 Bytes für die Länge lesen
    length_data = secure_recv_exact(sock, 4)
    if length_data is None:
        raise ConnectionError("Failed to read message length")

    length = struct.unpack('!I', length_data)[0]

    # Länge validieren
    if length > MAX_MESSAGE_SIZE:
        raise ValueError(f"Message too large: {length}")

    # Exakte Nachrichtenlänge lesen
    message = secure_recv_exact(sock, length)
    if message is None:
        raise ConnectionError("Failed to read message body")

    return message

def secure_recv_exact(sock, size):
    """Empfängt genau 'size' Bytes."""
    buffer = b''
    while len(buffer) < size:
        chunk = sock.recv(size - len(buffer))
        if not chunk:
            return None  # Verbindung geschlossen
        buffer += chunk
    return buffer
// SICHER: Behandelt partielles Lesen von InputStream
public class SecureReader {

    public byte[] readExact(InputStream in, int size) throws IOException {
        byte[] buffer = new byte[size];
        int totalRead = 0;

        while (totalRead < size) {
            int bytesRead = in.read(buffer, totalRead, size - totalRead);

            // SICHER: EOF behandeln
            if (bytesRead == -1) {
                throw new EOFException(
                    "Expected " + size + " bytes, got " + totalRead);
            }

            totalRead += bytesRead;
        }

        return buffer;
    }

    // Alternative: DataInputStream verwenden, der dies behandelt
    public byte[] readExactWithDataStream(InputStream in, int size)
            throws IOException {
        DataInputStream dis = new DataInputStream(in);
        byte[] buffer = new byte[size];
        dis.readFully(buffer);  // Wirft EOFException wenn nicht genug Bytes
        return buffer;
    }
}

CVE-Beispiele

  • CVE-2004-1395 -- Null-Byte- und andere Längenpakete verursachen bei recvfrom einen unerwarteten Rückgabecode, der die Empfangsschleife des Servers beendet.
  • CVE-2002-2124 -- Ungeprüfter recv()-Rückgabecode löst eine Endlosschleife aus.
  • CVE-2005-1858 -- read(), das weniger Bytes als erwartet zurückgibt, lässt Speicher ungelöscht.
  • CVE-2000-0536 -- Zugriffsbeschränkung wird umgangen, wenn DNS-Reverse-Lookup keinen Hostnamen zurückgibt.

Referenzen

  1. MITRE Corporation. "CWE-394: Unexpected Status Code or Return Value." https://cwe.mitre.org/data/definitions/394.html