SQL-Injections
SQL-Injections bezeichnet das Ausnutzen von Sicherheitslücken im Zusammenhang mit SQL-Datenbanken, die durch mangelnde Überprüfung von Eingabeparameter entstehen. Diese Art der Sicherheitslücke zählt zu eine der häufigsten und ist oftmals besonders kritisch, da Angreifer so an sensible Daten eurer Nutzer gelangen können. Wie bei den meistens anderen Sicherheitslücken auch gilt bei SQL-Injections der Merkspruch: Traue niemals den Eingaben von Benutzern.
Grundlagen SQL-Injections
Um die Angriffe und den Schutz zu verdeutlichen Nutzen wir nachfolgenden die User-Tabelle aus unserem MySQL Tutorial (weitere Infos).
Ein häufiger Fehler vieler Programmierung ist die falsche Dynamisierung von SQL-Queries. Einem Script wird ein gewisser Wert übergeben, beispielsweise mittels GET-Parameter eine ID. Um nun den Datensatz mit der entsprechenden ID abzufragen wird oft ein Code wie folgt verwendet:
Der einfachste Gedanke wäre wie folgt (diese Variante ist nicht zu empfehlen):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php $pdo = new PDO('mysql:host=localhost;dbname=databasename', 'username', 'password'); if(isset($_GET['id'])) { $id = $_GET['id']; } else { die("Bitte eine ?id übergeben"); } $sql = "SELECT email, vorname, nachname FROM users WHERE id = $id"; foreach ($pdo->query($sql) as $row) { echo $row['vorname']." ".$row['nachname']."<br />"; echo "E-Mail: ".$row['email']."<br /><br />"; } ?> |
Das Problem liegt hier in Zeile 10. Ruft ihr die Seite mit folgendem Parameter auf:
seite.php?id=1 OR id > 1
So wird der folgende SQL Befehl erzeugt und an die Datenbank gesendet:
1 |
SELECT email, vorname, nachname FROM users WHERE id = 1 OR id > 1 |
Das Resultat davon ist die Ausgabe aller Benutzer statt nur einem einzelnen Nutzer. Dies mag vielleicht nun nicht so tragisch sein, aber ein Angreifer kann nun viel Unfug treiben. Der Aufruf der Seite mittels:
seite.php?id=-1 UNION SELECT email, vorname, passwort AS nachname FROM user
Daraus wird der folgende SQL-Query gebaut und an die Datenbank gesendet:
1 |
SELECT email, vorname, nachname FROM users WHERE id =-1 UNION SELECT email, vorname, passwort AS nachname FROM user |
Mittels der UNION-Anweisung werden zwei SQL-Queries miteinander verbunden. Der erste Query versucht die Information für den Nutzer '-1' zu finden, der nicht existiert. Der zweite Query fragt erneut eure User-Tabelle ab, aber in diesem Fall wird das Passwort-Feld umbenannt in das Nachname-Feld. Der nachfolgende Script, der ja den harmlosen Nachnamen ausgibt, gibt nun alle E-Mail-Adressen, Vornamen und Passwörter eurer User aus.
Durch diese SQL-Injection kann ein Angreifer an sämtliche Daten eurer Datenbank gelangen.
Nicht nur SELECT-Anweisungen sind gefährdet, sondern sämtliche Daten die ihr an die Datenbank sendet. So können auch UPDATE, INSERT und DELETE-Anweisungen entsprechend manipuliert werden. Habt ihr beispielsweise den folgenden SQL-Query:
1 |
$sql = "UPDATE user SET vorname='".$_POST['neuer_vorname']."' WHERE id = ".$_SESSION['userid']; |
Hier kann ein Angreifer zwar nicht unbedingt die Session manipulieren, aber dieser kann den Wert von neuer_vorname manipulieren. Gibt er im Formularfeld beispielsweise folgenden Wert ein:
1 |
You got hacked', passwort='Hacker Passwort' WHERE id=1 -- |
So sieht der an die Datenbank gesendete SQL-Query wie folgt aus (angenommen der Angreifer hat die User-ID 374):
1 |
UPDATE user SET vorname='You got hacked', passwort='Hacker Passwort' WHERE id=1 -- ' WHERE id = 374 |
Die zwei Bindestriche -- kommentieren die nachfolgenden Anweisungen aus. Mit solch einer Eingabe lässt sich der Vorname und das Passwort von beliebigen Benutzern in eurer User-Tabelle verändern. So kann der Angreifer beispielsweise das Passwort von einem Administrator überschreiben und erhält dadurch Zugriff auf einen Administratoraccount, womit weiterer Schaden angerichtet werden kann.
Schutz vor SQL-Injections
Der beste Schutz vor SQL-Injections ist mittels Prepared Statements gegeben. Bei prepared Statements werden die Parameter, im den obigen Beispielen die ID oder der neue Vorname, seperat vom SQL-Query an die Datenbank gesendet. Dadurch ist gewährleistet, dass eingeschleuster Code in die Variable keinen Effekt auf den Query hat.
Es ist stark zu empfehlen, jede Query mit Nutzerdaten als Prepared Statement auszuführen. Versucht nicht in Versuchung zu kommen den SQL-Query selber zu konstruieren, da dies Fehleranfällig ist. Durch die (richtige) Verwendung von Prepared Statements seid ihr gegen SQL-Injections geschützt.
Validierung von Eingaben
Nach eigener Erfahrung lassen sich 98% der Queries mittels Prepared Statements konstruieren und sind somit sicher für SQL-Injections. Ein paar spezielle Queries existieren dennoch, bei denen man nicht auf Prepared Statements zurückgreifen kann. Möchte man dem Besucher z.B. erlauben eine Tabelle basierend auf frei wählbare Spalten zu durchsuchen, so steckt das dynamische Element in der Struktur des Queries und nicht im Parameter.
Eine unsichere Variante für solch eine Suchfunktion wäre:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php $pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password'); $suchspalte = $_GET['suchspalte']; //z.B. die Spalte vorname $suchwort = $_GET['suchwort']; //z.B. den Namen Max $statement = $pdo->prepare("SELECT * FROM users WHERE ".$suchspalte." LIKE :vorname"); $statement->execute(array(':vorname' => "%$suchwort%")); while($row = $statement->fetch()) { echo $row['vorname']." ".$row['nachname']."<br />"; echo "E-Mail: ".$row['email']."<br /><br />"; } ?> |
Der Parameter suchwort ist zwar vor SQL-Injections geschützt, aber über den Parameter suchspalte kann ein Angreifer beliebigen Schaden anrichten. Dieses per Prepared Statement abzusichern ist leider nicht möglich. Deswegen müssen wir entweder fixe SQL-Queries nutzen oder den Wert von $suchspalte entsprechend validieren.
Eine sichere Variante ist die folgende:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php $pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password'); $suchspalte = $_GET['suchspalte']; //z.B. die Spalte vorname $suchwort = $_GET['suchwort']; //z.B. den Namen Max $erlaubte_spalten = array('vorname', 'nachname', 'email'); if(!in_array($suchspalte, $erlaubte_spalten)) { die('Ungültiger Parameter für $suchwort'); } $statement = $pdo->prepare("SELECT * FROM users WHERE ".$suchspalte." LIKE :suchwort"); $statement->execute(array(':suchwort' => "%$suchwort%")); while($row = $statement->fetch()) { echo $row['vorname']." ".$row['nachname']."<br />"; echo "E-Mail: ".$row['email']."<br /><br />"; } ?> |
Hier wird ein Array mit erlaubten Werten erzeugt und nur wenn wir solch einen Wert vorliegend haben, wird das weitere Script ausgeführt. Falls ein Angreifer versuchen würde einen anderen Wert für $suchspalte zu übergeben, würde unser Script mittels der Funktion die() abbrechen.
Autor: Nils Reimers