Es ist ein leidiges Thema, aber ich finde man kann es nicht oft genug erwähnen und schreiben. Es geht um Sicherheit im Hinblick auf die Datenverarbeitung von nicht bekannten Inhalten.

Jeder Entwickler, ob Anfänger oder Profi wir irgendwann mal in der Situation sein, in der er vom User eingegebene oder  über eine Schnittstelle eingelesene Daten in einer Datenbank speichern möchte, oder diese für eine Abfrage benötigt.

Eins haben die beiden Szenarien gemeinsam. Wir haben keinen Einfluss auf den Inhalt der Daten.



Die Testumgebung

Ich setzte hier einmal als Datenbank MySQL  und als Programmiersprache PHP ab Version 5.1 voraus. Die 5.1er Version deshalb, weil ab dieser Version PDO (PHP Data Objects) unterstützt wird.

PDO ist eine Datenbankschnittstelle, welche den Vorteil bietet allgemeingültig für alle Datenbanken zu sein. Wichtig für diese Beispiele ist die Unterstützung von Prepared Statements. Aber herzu später mehr.

Prepared Statements werden aber natürlich auch in anderen Sprachen wie z.B. Java unterstütz. PHP soll hier nur als Beispiel dienen. Die Ergebnisse aber sind in (fast) allen Sprachen umsetzbar.

Weiterhin ist der Beispiel-Code hier extrem vereinfacht. Rückgabewerte werden nicht ausgelesen Exeptions wie z.B. die PDOException werden nicht abgefangen etc.



Das Szenario

Nehmen wir einmal den einfachen Fall an es handelt sich um ein Login-Formular und der User soll seinen Usernamen und sein Passwort eingeben.

Mit diesen Daten fragen wir dann in unserer Datenbank an und erhalten ggf. den eindeutigen User. Hier geht es also nicht einmal um das Abspeichern von Daten, sondern um eine simple SQL-Select-Anweisung.

Der User gibt also “admin” als Usernamen und  ”test” als Passwort ein. Wir würden also die beiden Parameter auslesen und in eine SQL-Anweisung packen. Das Passwort habe ich hier wie in meinem Artikel über sicheres Passwort-Handling beschreiben als MD5 verschleiert.

Als erstes initiieren wir die Datenbank:

// Der Connection-String zur DB - hier mit dem treiber für mysql
$db_conn = "mysql:host=localhost;dbname=test_db";
// der User unserer Anwendung
$db_user = "test_app";
// das Passwort (frei erfunden ;) )
$db_pass = "sxGhrZdD"; 

// Datenbank-Objekt initiieren
$db = new PDO("mysql:host=localhost;dbname=test_db", $db_user, $db_pass);

Dann lesen wir die Parameter (vereinfacht) aus und bauen das SQL-Statement zusammen und schicken das Ganze an die Datenbank:

// Post-Paramter auslesen (vereinfacht)
$username = $_POST["username"];
$password = $_POST["password"];
// Passwort als md5 verschleiern
$password = md5($password);

// SQL zusammenbauen
$sql = "SELECT * FROM user where username='".$username."' and password='".$password."';";

// Datenbank-Abfrage
$db->query($sql);

Die Anfrage

SELECT * FROM user where username='admin' and password='098f6bcd4621d373cade4e832627b4f6';

wird nun an die Datenbank gesendet und ggf. wird ein User zurückgegeben, oder auch nicht. Aber das spielt für unser Beispiel überhaupt keine Rolle. Wir wollen nämlich nun weiter annehmen, dass der User in dem Eingabefeld mit dem Namen username nicht “admin”, sondern

'; delete from user; --

eingibt. Das führt nun laut unserem Code zu folgender Datenbank-Anfrage:

SELECT * FROM user where username=''; delete from user; --' and password='098f6bcd4621d373cade4e832627b4f6';



Der Schock

dürfte relativ hoch sein. Das Resultat nach dem Abschicken der Anfrage ist verehrend. Die gesamte Tabelle user ist leer.  Das Problem, welches zu der leeren Datenbank führt nennt sich SQL-Injection und darf, wie Ihr schon an diesem einfachen Beispiel seht, auf keinen Fall unterschätzt werden. Die Daten der Tabelle user währen in unserem Falle (wenn kein Backup existiert) für immer weg.

Der Grund für das Verschwinden der Daten ist einfach der, dass der Datenbank-Server mehrere Anfragen hintereinander ausführt, wenn diese durch ein Semikolon getrennt sind. In unserem Fall werden durch die Manipulation zwei anstelle von einer Anfrage abgeschickt:

  1. SELECT * FROM user where username=”;
    Diese Anfrage bewirkt rein gar nichts und ist auch nicht relevant.
  2. delete from user;
    Das ist die gefährliche Anfrage, die alle User löscht.
  3. –’ and password=’098f6bcd4621d373cade4e832627b4f6′;
    Dieser letzte Teil ist eigentlich gar keine Anfrage, da er mit einem ‘–’ beginnt. Dieser doppelte Minus-Zeichen leitet in SQL einfach einen Kommentar ein.



Die Lösung(en)

Aber was ist passiert?

Unsere schöne Anfrage wurde einfach durch das erste vom User eingegeben Zeichen (dem einfachen Anführungszeichen)  unterbrochen und war danach beliebig manipulierbar. Hier wäre es zwingend nötig die Usereingabe für die Datenbank-Anfrage zu escapen oder auf Deutsch zu maskieren.

Hierbei werden spezielle reservierte Zeichen wie unter anderem auch das einfache Anführungszeichen mit einem vorangestellten Backslash (‘\’) maskiert und somit für die Datenbank-Anfrage aufbereitet. In unserem Beispiel würde dann die SQL-Anfrage so lauten

SELECT * FROM user where username='\'; delete from user; --' and password='098f6bcd4621d373cade4e832627b4f6';

und wäre somit ungefährlich. Hier würde also der gesamte vom User eingegeben String als echter String überprüft. Das doppelte Minus-Zeichen ist hier auch ungefährlich geworden, da es nun mit zu dem zu überprüfenden String gehört.

Zum escapen gibt es nun unterschiedliche Methoden. !Achtung! verwendet bitte auf keinen Fall addslashes, da diese Methode einfach dafür ungeeignet ist. Hier nun die richtigen Methoden:

  1. mysql_real_escape_string
    Diese Funktion maskiert alle wichtigen Zeichen für die SQL-Anfrage.
  2. PDO mit Prepared Statements
    Auf diese, meiner Meinung nach beste Methode möchte ich nun näher eingehen



PDO mit Prepared Statements

Die von PDO unterstützten Prepare Staements sind definitiv am besten für unser Problem geeignet. Einmal weil sie auch intern wie mysql_real_escape_string alle Werte maskiert und dass auch noch Treiber-Abhängig für jeden Datenbank-Typ (mysql_real_escape_string ist wie der Name schon vermuten lässt für MySQL otimiert) und zudem, weil Prepare Staements dafür sorgen, dass gleiche Anfragen nur einmal geparst werden müssen. Zudem kann sich die Datenbank dadurch auf gleichartige Anfragen vorbereiten.

Eine korrekte Benutzung für Prepare Staements sähe so aus:

// Prepare Statement generieren
$statement = $db->prepare("SELECT * FROM user WHERE username=:username and password=:password;");
// bindValue maskiert automatisch die Werte
$statement >bindValue(':username',$username);
$statement >bindValue(':password',$password);
// Statement abschicken
$statement >execute();
// analaog der Methode mysql_fetch_array die erste Zeile zurückgeben
$result = $statement->fetch();

Wie hier schön zu sehen ist wird das Statement erst vorbereitet indem wir nur Makros für die eigentlichen Werte angeben. Diese Makros sind in unserem Fall Doppelpunkte gefolgt von dem Namen.

Mit Hilfe von bindValue werden diese Werte dann mit maskiertem Leben gefüllt. Anschließend schicken wir das Statement mit execute an den MySQL- Server.

Dadurch haben wir jetzt also SQL-Injection verhindert. Am Schluss möchte ich euch aber noch ein paar weitere Tipps zur Datenbank-Sicherheit geben.



DB-Sicherheits Tipps

Im Folgenden möchte ich noch auf ein paar grundlegende Sicherheits-Tipps im Bezug auf das Arbeiten und die Kommunikation zwischen Applikationen und Datenbanken hinweisen.

  1. Daten-Rechte
    die Applikation sollte immer mit den für die benötigten Aktionen minimalsten Rechten versehen sein. Das sind in der Regel eigentlich nur Insert, Update, Select und Delete.  Ein Alter Table oder gar Drop hat hier in der Regel nichts verloren.
  2. Datenbank-Rechte
    die Applikation sollte natürlich nur auf die Datenbank und auf die Tabellen in dieser Datenbank Rechte besitzen auf die sie auch zugreifen muss. Alles andere ist tendenziell unsicher und unnötig.
  3. Backups erstellen
    das dürfte eigentlich jedem klar sein. Es gibt wohl nichts Schlimmeres als nach einem Jahr betrieb festzustellen, dass alle Daten unwiderruflich weg sind. Die meisten denken an Backups erst, wenn das Kinde schon in den Brunnen gefallen ist.

Ähnliche Artikel: