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*()
undrender*()
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']);