Sicherheitsrisiken

Datenbanken enthalten oft sensible Daten und ermöglichen die Durchführung gefährlicher Operationen. Die wichtigsten Aspekte für eine sichere Arbeit mit Nette Database sind:

  • Verstehen des Unterschieds zwischen sicherer und unsicherer API
  • Parametrisierte Abfragen verwenden
  • Ordnungsgemäße Validierung der Eingabedaten

Was ist eine SQL-Injektion?

SQL-Injection ist das größte Sicherheitsrisiko bei der Arbeit mit Datenbanken. Sie tritt auf, wenn ungefilterte Benutzereingaben Teil einer SQL-Abfrage werden. Ein Angreifer kann seine eigenen SQL-Befehle einfügen und dadurch:

  • Unbefugte Daten extrahieren
  • Daten in der Datenbank ändern oder löschen
  • die Authentifizierung zu 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 eingeben wie: ' OR '1'='1
// Die resultierende Abfrage würde lauten: SELECT * FROM users WHERE name = '' OR '1'='1'
// Dies gibt alle Benutzer zurück

Das Gleiche 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]'");

Sichere parametrisierte Abfragen

Der grundlegende Schutz gegen SQL-Injection sind parametrisierte Abfragen. Nette Database bietet mehrere Möglichkeiten, diese 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 anderen Methoden im Database Explorer, die das Einfügen von Ausdrücken mit Fragezeichenplatzhaltern und Parametern erlauben.

Für die Klauseln INSERT, UPDATE oder WHERE können Sie Werte in einem Array übergeben:

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

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

Validierung von Parameterwerten

Parametrisierte Abfragen sind der Eckpfeiler einer sicheren Datenbankarbeit. Die an sie übergebenen Werte müssen jedoch mehrere Validierungsstufen durchlaufen:

Typprüfung

Die Sicherstellung des korrekten Datentyps von Parametern ist entscheidend – dies ist eine notwendige Bedingung für die sichere Verwendung von Nette Database. Die Datenbank geht davon aus, dass alle Eingabedaten den richtigen Datentyp haben, der der Spalte entspricht.

Wenn zum Beispiel $name in den vorherigen Beispielen unerwartet ein Array statt einer Zeichenkette wäre, würde Nette Database versuchen, alle seine Elemente in die SQL-Abfrage einzufügen, was zu einem Fehler führen würde. Verwenden Sie daher niemals unvalidierte Daten aus $_GET, $_POST oder $_COOKIE direkt in Datenbankabfragen.

Format-Validierung

Auf der zweiten Ebene wird das Format der Daten überprüft. So wird beispielsweise sichergestellt, dass Zeichenketten UTF-8-kodiert sind und ihre Länge mit der Spaltendefinition übereinstimmt, oder es wird überprüft, ob numerische Werte in den zulässigen Bereich für den Datentyp der Spalte fallen.

Auf dieser Ebene können Sie sich teilweise auf die Datenbank selbst verlassen – viele Datenbanken weisen ungültige Daten zurück. Das Verhalten kann jedoch variieren: Einige schneiden lange Strings stillschweigend ab oder schneiden Zahlen ab, die außerhalb des zulässigen Bereichs liegen.

Bereichsspezifische Validierung

Die dritte Ebene umfasst logische Prüfungen, die für Ihre Anwendung spezifisch sind. So wird z. B. überprüft, ob Werte aus Auswahlfeldern mit den verfügbaren Optionen übereinstimmen, ob Zahlen in einen erwarteten Bereich fallen (z. B. Alter 0–150 Jahre) oder ob Beziehungen zwischen Werten sinnvoll sind.

  • Verwenden Sie Nette Forms, die automatisch eine korrekte Validierung aller Eingaben vornehmen.
  • Verwenden Sie Presenter und deklarieren Sie Parameter-Datentypen in action*() und render*() Methoden.
  • Oder implementieren Sie eine benutzerdefinierte Validierungsschicht mit Standard-PHP-Tools wie filter_var().

Sicheres Arbeiten mit Columns

Im vorigen Abschnitt haben wir behandelt, wie man Parameterwerte richtig validiert. Bei der Verwendung von Arrays in SQL-Abfragen muss jedoch auch auf die Schlüssel geachtet werden.

// ❌ GEFÄHRLICHER CODE - Array-Schlüssel werden nicht sanitized
$database->query('INSERT INTO users', $_POST);

Bei INSERT- und UPDATE-Befehlen ist dies eine große Sicherheitslücke – ein Angreifer kann jede Spalte in der Datenbank einfügen oder ändern. Er könnte z.B. is_admin = 1 einstellen oder beliebige Daten in sensible Spalten einfügen (bekannt als Mass Assignment Vulnerability).

Bei WHERE-Bedingungen ist es sogar noch gefährlicher, da sie Operatoren enthalten können:

// ❌ GEFÄHRLICHER CODE - Array-Schlüssel werden nicht bereinigt
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// führt Abfrage aus WHERE (`Gehalt` > 100000)

Ein Angreifer kann diesen Ansatz nutzen, um systematisch die Gehälter von Mitarbeitern zu ermitteln. Er könnte mit einer Abfrage nach Gehältern über 100.000, dann unter 50.000 beginnen und durch schrittweises Eingrenzen des Bereichs die ungefähren Gehälter aller Mitarbeiter ermitteln. Diese Art von Angriff wird als SQL-Aufzählung bezeichnet.

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

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

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

Spalte Whitelist

Für eine sichere Arbeit mit Spaltennamen benötigen Sie einen Mechanismus, der sicherstellt, dass Benutzer nur mit zugelassenen Spalten interagieren und keine eigenen hinzufügen können. Der Versuch, gefährliche Spaltennamen zu erkennen und zu blockieren (Blacklisting), ist unzuverlässig – ein Angreifer kann immer einen neuen Weg finden, einen gefährlichen Spaltennamen zu schreiben, den Sie nicht vorhergesehen haben.

Daher ist es viel sicherer, die Logik umzukehren und eine explizite Liste zulässiger Spalten zu definieren (Whitelisting):

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

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

// ✅ Kann jetzt sicher in Abfragen verwendet werden, wie z. B.:
$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. Dadurch wird sichergestellt, dass Bezeichner entsprechend der gegebenen Datenbanksyntax (z. B. unter Verwendung von Backticks in MySQL) korrekt escaped werden:

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

Wichtig: Verwenden Sie das Symbol ?name nur für vertrauenswürdige Werte, die im Anwendungscode definiert sind. Für Werte, die vom Benutzer bereitgestellt werden, verwenden Sie wiederum eine Whitelist. Andernfalls riskieren Sie Sicherheitslücken:

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