Sicherheitsrisiken

Datenbanken enthalten oft sensible Daten und ermöglichen die Durchführung gefährlicher Operationen. Für die sichere Arbeit mit Nette Database ist es entscheidend:

  • Den Unterschied zwischen sicherer und unsicherer API zu verstehen
  • Parametrisierte Abfragen zu verwenden
  • Eingabedaten korrekt zu validieren

Was ist SQL Injection?

SQL Injection ist das schwerwiegendste Sicherheitsrisiko bei der Arbeit mit Datenbanken. Es entsteht, wenn unbehandelte Benutzereingaben Teil einer SQL-Abfrage werden. Ein Angreifer kann eigene SQL-Befehle einschleusen und dadurch:

  • Unberechtigten Zugriff auf Daten erlangen
  • Daten in der Datenbank modifizieren oder löschen
  • Authentifizierung umgehen
// ❌ UNSICHERER CODE - anfällig für SQL-Injection
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Ein Angreifer kann beispielsweise den Wert eingeben: ' OR '1'='1
// Die resultierende Abfrage lautet dann: SELECT * FROM users WHERE name = '' OR '1'='1'
// Was alle Benutzer zurückgibt

Dasselbe gilt auch für den Database Explorer:

// ❌ UNSICHERER CODE - anfällig für SQL-Injection
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Parametrisierte Abfragen

Die grundlegende Verteidigung gegen SQL-Injection sind parametrisierte Abfragen. Nette Database bietet mehrere Möglichkeiten, sie zu verwenden.

Der einfachste Weg ist die Verwendung von Fragezeichen-Platzhaltern:

// ✅ Sichere parametrisierte Abfrage
$database->query('SELECT * FROM users WHERE name = ?', $name);

// ✅ Sichere Bedingung im Explorer
$table->where('name = ?', $name);

Dies gilt für alle weiteren Methoden im Database Explorer, die das Einfügen von Ausdrücken mit Fragezeichen-Platzhaltern und Parametern ermöglichen.

Für INSERT-, UPDATE-Befehle oder die WHERE-Klausel können wir Werte in einem Array übergeben:

// ✅ Sicherer INSERT
$database->query('INSERT INTO users', [
	'name' => $name,
	'email' => $email,
]);

// ✅ Sicherer INSERT im Explorer
$table->insert([
	'name' => $name,
	'email' => $email,
]);

Validierung von Parameterwerten

Parametrisierte Abfragen sind der grundlegende Baustein für die sichere Arbeit mit Datenbanken. Dennoch müssen die Werte, die Sie als Parameter übergeben, sorgfältig validiert werden, um andere Arten von Fehlern und potenziellen Problemen zu vermeiden.

Typkontrolle

Stellen Sie sicher, dass Parameter den erwarteten Datentyp haben. Nette Database versucht zwar, Typen zu konvertieren, aber die Übergabe eines völlig falschen Typs (z.B. ein Array, wo ein String erwartet wird) kann zu Fehlern oder unerwartetem Verhalten führen.

Wenn beispielsweise $name in den vorherigen Beispielen unerwartet ein Array anstelle einer Zeichenkette wäre, könnte dies einen Fehler auslösen. Verwenden Sie daher niemals unvalidierte Rohdaten aus $_GET, $_POST, $_COOKIE oder anderen externen Quellen direkt in Datenbankabfragen.

Formale und Wertebereichs-Kontrolle

Überprüfen Sie, ob die Daten das erwartete Format haben (z.B. gültige E-Mail-Adresse, UTF-8-Kodierung) und ob Werte innerhalb zulässiger Grenzen liegen (z.B. Länge einer Zeichenkette, Wertebereich einer Zahl).

Obwohl die Datenbank selbst einige dieser Prüfungen durchführen kann (z.B. durch Spaltentypen und Constraints), ist es besser, ungültige Daten bereits in der Anwendung abzufangen. Das Verhalten der Datenbank bei ungültigen Daten kann variieren (Fehler, stilles Abschneiden, etc.).

Domänen-/Logik-Kontrolle

Die dritte Ebene stellen logische Kontrollen dar, die spezifisch für Ihre Anwendung sind. Zum Beispiel die Überprüfung, ob Werte aus Select-Boxen den angebotenen Optionen entsprechen, ob Zahlen im erwarteten Bereich liegen (z. B. Alter 0–150 Jahre) oder ob gegenseitige Abhängigkeiten zwischen Werten sinnvoll sind.

Empfohlene Validierungsmethoden

  • Verwenden Sie Nette Forms, die eine robuste Validierung für Benutzereingaben bieten.
  • Nutzen Sie Type Hints in Presentern für action*() und render*() Methodenparameter.
  • Implementieren Sie eine eigene Validierungsschicht mit PHP-Funktionen wie filter_var(), mb_strlen(), regulären Ausdrücken oder spezialisierten Validierungsbibliotheken.

Sichere Arbeit mit Spaltennamen (Bezeichnern)

Während Parameterwerte durch Parametrisierung geschützt sind, müssen Spalten- und Tabellennamen (Bezeichner), wenn sie dynamisch sind, anders behandelt werden. Sie können nicht direkt durch Fragezeichen-Platzhalter ersetzt werden.

// ❌ UNSICHERER CODE - Schlüssel im Array sind nicht behandelt
$database->query('INSERT INTO users', $_POST);

Bei INSERT- und UPDATE-Befehlen ist dies ein kritischer Sicherheitsfehler – ein Angreifer kann jede beliebige Spalte in die Datenbank einfügen oder ändern. Er könnte sich beispielsweise is_admin = 1 setzen oder beliebige Daten in sensible Spalten einfügen (sog. Mass Assignment Vulnerability).

In WHERE-Bedingungen ist dies noch gefährlicher, da sie Operatoren enthalten können:

// ❌ UNSICHERER CODE - Schlüssel im Array sind nicht behandelt
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// führt die Abfrage WHERE (`salary` > 100000) aus

Ein Angreifer kann diesen Ansatz nutzen, um systematisch die Gehälter von Mitarbeitern zu ermitteln. Er beginnt beispielsweise mit einer Abfrage nach Gehältern über 100.000, dann unter 50.000 und durch schrittweise Eingrenzung des Bereichs kann er die ungefähren Gehälter aller Mitarbeiter aufdecken. Diese Art von Angriff wird als SQL-Enumeration bezeichnet.

Die Methoden where() und whereOr() sind noch viel flexibler und unterstützen in Schlüsseln und Werten SQL-Ausdrücke einschließlich Operatoren und Funktionen. Dies gibt einem Angreifer die Möglichkeit, eine SQL-Injection durchzuführen:

// ❌ UNSICHERER CODE - Angreifer kann eigenes SQL einschleusen
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// führt die Abfrage WHERE (0) UNION SELECT name, salary FROM users WHERE (1) aus

Dieser Angriff beendet die ursprüngliche Bedingung mit 0), fügt mit UNION ein eigenes SELECT hinzu, um sensible Daten aus der Tabelle users zu erhalten, und schließt die syntaktisch korrekte Abfrage mit WHERE (1) ab.

Whitelist für Spalten

Für die sichere Arbeit mit Spaltennamen benötigen wir einen Mechanismus, der sicherstellt, dass der Benutzer nur mit erlaubten Spalten arbeiten kann und keine eigenen hinzufügen kann. Wir könnten versuchen, gefährliche Spaltennamen zu erkennen und zu blockieren (Blacklist), aber dieser Ansatz ist unzuverlässig – ein Angreifer kann immer einen neuen Weg finden, einen gefährlichen Spaltennamen zu schreiben, den wir nicht vorhergesehen haben.

Daher ist es viel sicherer, die Logik umzukehren und eine explizite Liste erlaubter Spalten zu definieren (Whitelist):

// Spalten, die der Benutzer bearbeiten darf
$allowedColumns = ['name', 'email', 'active'];

// Wir entfernen alle nicht erlaubten Spalten aus der Eingabe
$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // Flip für O(1) lookup

// ✅ Nun können wir sicher in Abfragen verwenden, wie zum Beispiel:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);

Dynamische Bezeichner

Für dynamische Tabellen- und Spaltennamen verwenden Sie den Platzhalter ?name. Dieser stellt das korrekte Escaping von Bezeichnern gemäß der Syntax der jeweiligen Datenbank sicher (z. B. durch Backticks in MySQL):

// ✅ Sichere Verwendung vertrauenswürdiger Bezeichner
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Ergebnis in MySQL: SELECT `name` FROM `users`

Wichtig: Verwenden Sie das Symbol ?name nur für vertrauenswürdige Werte, die im Anwendungscode definiert sind. Für Werte vom Benutzer verwenden Sie wieder eine Whitelist. Andernfalls setzen Sie sich Sicherheitsrisiken aus:

// ❌ UNSICHER - verwenden Sie niemals Benutzereingaben
$database->query('SELECT ?name FROM users', $_GET['column']);
Version: 4.0