Unsachgemäße Bereinigung bei geworfener Exception

Beschreibung

Unsachgemäße Bereinigung bei geworfener Exception ist eine Schwachstelle, bei der das Produkt seinen Zustand nicht bereinigt oder seinen Zustand falsch bereinigt, wenn eine Exception geworfen wird, was zu unerwartetem Zustand oder Kontrollfluss führt. Wenn Code komplex wird und Ressourcenbereinigung an mehreren Stellen benötigt wird, können Exceptions den normalen Kontrollfluss unterbrechen und notwendige Bereinigungsoperationen verhindern. Dies hinterlässt die Anwendung in einem inkonsistenten Zustand mit nicht ordnungsgemäß freigegebenen Ressourcen, nicht befreiten Sperren oder nicht zurückgesetzten Flags.

Risiko

Unsachgemäße Exception-Bereinigung führt zu Ressourcenerschöpfung, Sicherheitsschwachstellen und unvorhersehbarem Anwendungsverhalten. Nicht freigegebene Sperren können Deadlocks in nebenläufigen Anwendungen verursachen. Nicht freigegebene Ressourcen sammeln sich an und verursachen Denial-of-Service. Sicherheitskritischer Zustand wie Authentifizierungs-Flags kann nach fehlgeschlagenen Operationen gesetzt bleiben und möglicherweise unbefugten Zugriff ermöglichen. Unvollständiger Transaktionszustand kann Datenkorruption verursachen. Das Risiko wird in langlebigen Server-Anwendungen verstärkt, wo geleakte Ressourcen sich im Laufe der Zeit ansammeln.

Lösung

Stellen Sie sicher, dass Bereinigung erfolgt, wenn Schleifen verlassen oder Funktionen via Exceptions beendet werden. Verwenden Sie try-finally-Blöcke (oder äquivalente Sprachkonstrukte wie try-with-resources in Java, using-Statements in C#, Context-Manager in Python), um zu garantieren, dass Bereinigungscode unabhängig von Exceptions ausgeführt wird. Verwenden Sie RAII (Resource Acquisition Is Initialization)-Muster in C++, um Ressourcen-Lebensdauer an Objekt-Scope zu binden. Gestalten Sie Bereinigungscode idempotent, damit er sicher mehrmals ausgeführt werden kann. Erwägen Sie die konservative Verwendung von Exceptions anstatt sich auf sie für normalen Kontrollfluss zu verlassen.

Häufige Auswirkungen

AuswirkungDetails
SonstigesUmfang: Sonstiges

Unerwarteter Zustand - Der Code könnte in einem schlechten Zustand hinterlassen werden mit nicht freigegebenen Ressourcen, gehaltenen Sperren oder falsch gesetzten Flags.
VerfügbarkeitUmfang: Verfügbarkeit

DoS: Ressourcenverbrauch - Ressourcen, die bei Exception nicht freigegeben werden, sammeln sich an und verursachen schließlich Ressourcenerschöpfung.
ZugriffskontrolleUmfang: Zugriffskontrolle

Privilegien erlangen - Sicherheitskritischer Zustand, der bei Exception nicht ordnungsgemäß zurückgesetzt wird, kann unbefugten Zugriff ermöglichen.

Beispielcode

Anfälliger Code

// Anfällig: Sperre wird bei Exception nicht freigegeben
public class VulnerableLockHandler {

    private boolean threadLock = false;

    public void processData(Data data) throws ProcessingException {
        // Sperre erwerben
        threadLock = true;

        try {
            // Verarbeitung die werfen kann
            validateData(data);
            transformData(data);
            storeData(data);

            // Anfällig: Gibt Sperre nur bei Erfolg frei
            threadLock = false;

        } catch (ValidationException e) {
            // Anfällig: threadLock bleibt true!
            throw new ProcessingException("Validierung fehlgeschlagen", e);
        } catch (TransformException e) {
            // Anfällig: threadLock bleibt true!
            throw new ProcessingException("Transformation fehlgeschlagen", e);
        }
        // Wenn storeData() wirft, wird Sperre nie freigegeben
    }
}
# Anfällig: Datenbank-Transaktion wird bei Exception nicht zurückgerollt
class VulnerableTransactionHandler:
    def update_records(self, records):
        self.db.begin_transaction()

        try:
            for record in records:
                self.validate_record(record)  # Kann werfen
                self.db.update(record)        # Kann werfen

            self.db.commit()

        except ValidationError as e:
            # Anfällig: Transaktion nicht zurückgerollt
            # Datenbank in inkonsistentem Zustand
            raise

        except DatabaseError as e:
            # Anfällig: Teilweise Updates committed
            # oder Transaktion bleibt offen
            raise

    def authenticate_user(self, username, password):
        self.auth_in_progress = True
        self.current_user = username

        try:
            user = self.lookup_user(username)  # Kann werfen
            valid = self.check_password(user, password)  # Kann werfen

            if valid:
                self.authenticated = True
            else:
                self.authenticated = False

        except UserNotFoundError:
            # Anfällig: auth_in_progress und current_user nicht zurückgesetzt
            raise

        except Exception:
            # Anfällig: Zustandsvariablen bleiben gesetzt
            raise

        finally:
            # Dieser finally-Block fehlt!
            pass

        self.auth_in_progress = False
        # Wird nie erreicht wenn Exception geworfen wird
// Anfällig: Datei-Handle und Speicher bei Fehler geleakt
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf exception_env;

typedef struct {
    char* data;
    size_t size;
} FileContent;

FileContent* vulnerable_read_file(const char* path) {
    FILE* file = fopen(path, "r");
    if (!file) {
        longjmp(exception_env, 1);  // Exception "werfen"
    }

    FileContent* content = malloc(sizeof(FileContent));
    if (!content) {
        // Anfällig: Datei-Handle geleakt
        longjmp(exception_env, 2);
    }

    fseek(file, 0, SEEK_END);
    content->size = ftell(file);
    fseek(file, 0, SEEK_SET);

    content->data = malloc(content->size);
    if (!content->data) {
        // Anfällig: content-Struct geleakt, Datei-Handle geleakt
        longjmp(exception_env, 3);
    }

    if (fread(content->data, 1, content->size, file) != content->size) {
        // Anfällig: Alle Ressourcen geleakt
        longjmp(exception_env, 4);
    }

    fclose(file);
    return content;
}
// Anfällig: Verbindung und Transaktion werden nicht bereinigt
public class VulnerableOrderProcessor {

    private SqlConnection connection;
    private SqlTransaction transaction;
    private bool processingActive = false;

    public void ProcessOrder(Order order) {
        processingActive = true;

        connection = new SqlConnection(connectionString);
        connection.Open();
        transaction = connection.BeginTransaction();

        try {
            ValidateOrder(order);      // Kann werfen
            ReserveInventory(order);   // Kann werfen
            ChargePayment(order);      // Kann werfen
            CompleteOrder(order);      // Kann werfen

            transaction.Commit();
            processingActive = false;
            connection.Close();

        } catch (ValidationException ex) {
            // Anfällig: Transaktion nicht zurückgerollt
            // Verbindung nicht geschlossen
            // processingActive noch true
            throw;
        } catch (PaymentException ex) {
            // Anfällig: Inventar reserviert aber nicht freigegeben
            // Transaktion teilweise abgeschlossen
            throw;
        }
    }
}

Korrigierter Code

// Korrigiert: Sperre ordnungsgemäß mit try-finally freigegeben
public class SecureLockHandler {

    private boolean threadLock = false;

    public void processData(Data data) throws ProcessingException {
        // Sperre erwerben
        threadLock = true;

        try {
            validateData(data);
            transformData(data);
            storeData(data);

        } catch (ValidationException e) {
            throw new ProcessingException("Validierung fehlgeschlagen", e);

        } catch (TransformException e) {
            throw new ProcessingException("Transformation fehlgeschlagen", e);

        } finally {
            // Korrigiert: Sperre wird immer freigegeben
            threadLock = false;
        }
    }

    // Korrigiert: ReentrantLock für bessere Kontrolle verwenden
    private final ReentrantLock lock = new ReentrantLock();

    public void processDataWithLock(Data data) throws ProcessingException {
        lock.lock();
        try {
            validateData(data);
            transformData(data);
            storeData(data);
        } finally {
            lock.unlock();  // Wird immer freigegeben
        }
    }
}
# Korrigiert: Ordnungsgemäße Bereinigung bei Exception
class SecureTransactionHandler:
    def update_records(self, records):
        self.db.begin_transaction()

        try:
            for record in records:
                self.validate_record(record)
                self.db.update(record)

            self.db.commit()

        except Exception:
            # Korrigiert: Bei Exception immer zurückrollen
            self.db.rollback()
            raise

    # Korrigiert: Context-Manager-Muster verwenden
    @contextmanager
    def transaction(self):
        self.db.begin_transaction()
        try:
            yield
            self.db.commit()
        except Exception:
            self.db.rollback()
            raise

    def update_records_with_context(self, records):
        with self.transaction():
            for record in records:
                self.validate_record(record)
                self.db.update(record)

    def authenticate_user(self, username, password):
        self.auth_in_progress = True
        self.current_user = username

        try:
            user = self.lookup_user(username)
            valid = self.check_password(user, password)

            if valid:
                self.authenticated = True
            else:
                self.authenticated = False
                self.current_user = None

        except Exception:
            # Korrigiert: Zustand bei jeder Exception zurücksetzen
            self.authenticated = False
            self.current_user = None
            raise

        finally:
            # Korrigiert: In-Progress-Flag immer zurücksetzen
            self.auth_in_progress = False
// Korrigiert: Ordnungsgemäße Bereinigung mit goto-Muster
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char* data;
    size_t size;
    int error;
    char error_msg[256];
} FileResult;

FileResult secure_read_file(const char* path) {
    FileResult result = {NULL, 0, 0, ""};
    FILE* file = NULL;
    char* data = NULL;

    file = fopen(path, "r");
    if (!file) {
        result.error = 1;
        snprintf(result.error_msg, sizeof(result.error_msg),
                 "Datei kann nicht geöffnet werden");
        goto cleanup;
    }

    fseek(file, 0, SEEK_END);
    result.size = ftell(file);
    fseek(file, 0, SEEK_SET);

    data = malloc(result.size);
    if (!data) {
        result.error = 2;
        snprintf(result.error_msg, sizeof(result.error_msg),
                 "Speicherallokation fehlgeschlagen");
        goto cleanup;
    }

    if (fread(data, 1, result.size, file) != result.size) {
        result.error = 3;
        snprintf(result.error_msg, sizeof(result.error_msg),
                 "Lesen fehlgeschlagen");
        goto cleanup;
    }

    result.data = data;
    data = NULL;  // Eigentum übertragen

cleanup:
    // Korrigiert: Ressourcen immer bereinigen
    if (data) {
        free(data);
    }
    if (file) {
        fclose(file);
    }

    return result;
}

// Korrigiert: C++ RAII-Ansatz
#ifdef __cplusplus
class FileHandle {
    FILE* file;
public:
    FileHandle(const char* path, const char* mode)
        : file(fopen(path, mode)) {}

    ~FileHandle() {
        if (file) fclose(file);
    }

    FILE* get() { return file; }
    operator bool() { return file != nullptr; }
};

std::vector<char> secure_read_file_cpp(const char* path) {
    FileHandle file(path, "r");  // RAII: automatisch geschlossen
    if (!file) {
        throw std::runtime_error("Datei kann nicht geöffnet werden");
    }

    // Auch wenn Exception geworfen wird, wird Datei durch Destruktor geschlossen
    fseek(file.get(), 0, SEEK_END);
    size_t size = ftell(file.get());
    fseek(file.get(), 0, SEEK_SET);

    std::vector<char> data(size);
    if (fread(data.data(), 1, size, file.get()) != size) {
        throw std::runtime_error("Lesen fehlgeschlagen");
    }

    return data;
}
#endif
// Korrigiert: Ordnungsgemäße Bereinigung mit try-finally und using
public class SecureOrderProcessor {

    public void ProcessOrder(Order order) {
        bool processingActive = false;
        SqlConnection connection = null;
        SqlTransaction transaction = null;
        bool inventoryReserved = false;

        try {
            processingActive = true;

            connection = new SqlConnection(connectionString);
            connection.Open();
            transaction = connection.BeginTransaction();

            ValidateOrder(order, transaction);

            ReserveInventory(order, transaction);
            inventoryReserved = true;

            ChargePayment(order, transaction);
            CompleteOrder(order, transaction);

            transaction.Commit();

        } catch (Exception) {
            // Korrigiert: Kompensierende Aktionen für teilweise Fertigstellung
            if (inventoryReserved && transaction != null) {
                try {
                    ReleaseInventory(order, transaction);
                } catch { /* Loggen aber ursprüngliche Exception nicht maskieren */ }
            }

            // Korrigiert: Transaktion zurückrollen
            if (transaction != null) {
                try {
                    transaction.Rollback();
                } catch { /* Loggen aber ursprüngliche Exception nicht maskieren */ }
            }

            throw;

        } finally {
            // Korrigiert: Immer bereinigen
            processingActive = false;

            transaction?.Dispose();
            connection?.Close();
            connection?.Dispose();
        }
    }

    // Korrigiert: Using-Muster für saubereren Code
    public void ProcessOrderWithUsing(Order order) {
        using (var connection = new SqlConnection(connectionString))
        using (var transaction = connection.BeginTransaction()) {
            try {
                connection.Open();

                ValidateOrder(order, transaction);
                ReserveInventory(order, transaction);
                ChargePayment(order, transaction);
                CompleteOrder(order, transaction);

                transaction.Commit();

            } catch {
                transaction.Rollback();
                throw;
            }
        }
        // Verbindung und Transaktion werden automatisch disposed
    }
}

CVE-Beispiele

Keine spezifischen CVEs sind in der MITRE-Datenbank für dieses CWE aufgeführt. Das Muster ist jedoch häufig bei:

  • Datenbank-Transaktionsbehandlungs-Schwachstellen
  • Sperrenverwaltungsfehler in nebenläufigen Anwendungen
  • Ressourcenbereinigungsfehler in Exception-Pfaden

Referenzen

  1. MITRE Corporation. "CWE-460: Improper Cleanup on Thrown Exception." https://cwe.mitre.org/data/definitions/460.html
  2. CERT Oracle Secure Coding Standard for Java. "ERR05-J. Do not let checked exceptions escape from a finally block."