Sicherheitsrisiken

Datenbanken enthalten oft sensible Daten und erlauben gefährliche Operationen. Für die sichere Arbeit mit Nette Database ist es entscheidend:

  • den Unterschied zwischen sicheren und unsicheren APIs zu verstehen
  • parametrisierte Queries zu verwenden
  • Eingabedaten richtig zu validieren

Was ist SQL-Injection?

SQL-Injection ist das schwerwiegendste Sicherheitsrisiko bei der Arbeit mit Datenbanken. Sie entsteht, wenn ungeprüfte Benutzereingaben Teil einer SQL-Query werden. Ein Angreifer kann eigene SQL-Befehle einschleusen und dadurch:

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

// Ein Angreifer könnte einen Wert wie diesen eingeben: ' OR '1'='1
// Die entstehende Query wäre: SELECT * FROM users WHERE name = '' OR '1'='1'
// Das gibt alle Benutzer zurück

Dasselbe gilt für den Database Explorer:

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

Parametrisierte Queries

Die grundlegende Abwehr gegen SQL-Injection sind parametrisierte Queries. Nette Database bietet mehrere Wege, sie zu verwenden.

Am einfachsten sind Fragezeichen-Platzhalter:

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

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

Das gilt für alle weiteren Methoden im Database Explorer, die das Einsetzen von Ausdrücken mit Fragezeichen-Platzhaltern und Parametern erlauben.

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

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

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

Validierung von Parameterwerten

Parametrisierte Queries sind der Grundstein sicherer Datenbankarbeit. Die Werte, die wir in sie einsetzen, müssen jedoch mehrere Prüfebenen durchlaufen:

Typkontrolle

Das Wichtigste ist, den korrekten Datentyp der Parameter sicherzustellen – das ist eine notwendige Bedingung für die sichere Verwendung von Nette Database. Die Datenbank geht davon aus, dass alle Eingabedaten den korrekten, zur jeweiligen Spalte passenden Datentyp haben.

Wäre $name in den vorigen Beispielen unerwartet ein Array statt eines Strings, würde Nette Database versuchen, alle seine Elemente in die SQL-Query einzusetzen, was zu einem Fehler führt. Verwenden Sie deshalb niemals unvalidierte Daten aus $_GET, $_POST oder $_COOKIE direkt in Datenbankabfragen.

Formale Kontrolle

Auf der zweiten Ebene prüfen wir das Format der Daten – zum Beispiel, ob Strings in der Kodierung UTF-8 vorliegen und ihre Länge der Spaltendefinition entspricht oder ob numerische Werte im zulässigen Bereich des Datentyps der Spalte liegen.

Auf dieser Ebene können wir uns teilweise auf die Datenbank selbst verlassen – viele Datenbanken weisen ungültige Daten zurück. Das Verhalten kann jedoch unterschiedlich sein; manche kürzen lange Strings stillschweigend oder beschneiden Zahlen außerhalb des Bereichs.

Domänenspezifische Kontrolle

Die dritte Ebene umfasst logische Prüfungen, die für Ihre Anwendung spezifisch sind. Zum Beispiel die Prüfung, ob Werte aus Auswahlfeldern den angebotenen Optionen entsprechen, ob Zahlen im erwarteten Bereich liegen (z. B. Alter 0–150 Jahre) oder ob die gegenseitigen Abhängigkeiten zwischen Werten sinnvoll sind.

Empfohlene Validierungsmethoden

  • Verwenden Sie Nette Forms, die automatisch für die richtige Validierung aller Eingaben sorgen.
  • Verwenden Sie Presenter und geben Sie die Datentypen der Parameter in den Methoden action*() und render*() an.
  • Oder implementieren Sie eine eigene Validierungsschicht mit den Standardwerkzeugen von PHP wie filter_var().

Sichere Arbeit mit Spalten

Im vorigen Abschnitt haben wir gezeigt, wie man Parameterwerte richtig validiert. Bei der Verwendung von Arrays in SQL-Queries müssen wir jedoch ihren Schlüsseln dieselbe Aufmerksamkeit widmen.

// ❌ GEFÄHRLICHER CODE - die Schlüssel im Array sind nicht geprüft
$database->query('INSERT INTO users', $_POST);

Bei INSERT- und UPDATE-Befehlen ist das ein kritischer Sicherheitsmangel – ein Angreifer kann beliebige Spalten der Datenbank einfügen oder ändern. Er könnte zum Beispiel is_admin = 1 setzen oder beliebige Daten in sensible Spalten schreiben (die sogenannte Mass Assignment Vulnerability).

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

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

Ein Angreifer kann auf diesem Weg systematisch die Gehälter der Mitarbeiter herausfinden. Er könnte zum Beispiel mit einer Abfrage nach Gehältern über 100 000 beginnen, dann unter 50 000, und indem er den Bereich schrittweise eingrenzt, die ungefähren Gehälter aller Mitarbeiter aufdecken. Diese Art von Angriff nennt man SQL-Enumeration.

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

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

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

Whitelist für Spalten

Für die sichere Arbeit mit Spaltennamen brauchen wir einen Mechanismus, der sicherstellt, dass der Benutzer nur mit erlaubten Spalten arbeiten kann und keine eigenen ergänzen darf. Wir könnten versuchen, gefährliche Spaltennamen zu erkennen und zu blockieren (Blacklist), aber dieser Ansatz ist unzuverlässig – ein Angreifer kann sich immer eine neue Schreibweise für einen gefährlichen Spaltennamen ausdenken, an die wir nicht gedacht haben.

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

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

// Alle unerlaubten Spalten aus der Eingabe entfernen
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));

// ✅ Jetzt sicher in Queries verwendbar, etwa:
$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. Er sorgt für das korrekte Escaping der Bezeichner nach der Syntax der jeweiligen Datenbank (in MySQL etwa mit Backticks):

// ✅ 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 Code der Anwendung definiert sind. Für Werte vom Benutzer verwenden Sie wieder eine Whitelist. Sonst setzen Sie sich Sicherheitsrisiken aus:

// ❌ GEFÄHRLICH - verwenden Sie niemals Benutzereingaben
$database->query('SELECT ?name FROM users', $_GET['column']);
Version: 4.x