PHP und MySQL: SQL-Injection vermeiden mit PDO

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- String-Verkettung in SQL-Abfragen ist die Einladung zur SQL-Injection.
- PDO mit Prepared Statements trennt Befehl und Daten sauber voneinander.
- Parameter bindet man mit bindValue, bindParam oder als Array in execute.
- mysql_real_escape_string ist nur die Notlösung, wenn PDO nicht zur Verfügung steht.
Heute geht es um ein Thema, das eigentlich jeder kennen sollte und das trotzdem in erschreckend vielen Skripten falsch gemacht wird: SQL-Injection. Gerade für Einsteiger ist das gefährlich, weil das Skript ja „funktioniert“ und man den Fehler nicht sieht. Ich habe mir in letzter Zeit ein paar Beispielcodes aus dem Netz angeschaut und war ziemlich überrascht, wie oft da noch Benutzereingaben direkt in die Abfrage wandern.
Das Problem
Ein typisches Login-Beispiel:
<?php
$name = $_POST['name'];
$pass = $_POST['pass'];
$sql = "SELECT * FROM benutzer WHERE name = '" . $name . "' AND pass = '" . md5($pass) . "'";
$result = mysql_query($sql);Gibt jemand als Namen admin' -- ein, sieht die Abfrage so aus:
SELECT * FROM benutzer WHERE name = 'admin' --' AND pass = '...'Alles hinter -- ist Kommentar, die Passwortprüfung fällt einfach weg. Und das ist noch das harmloseste Beispiel, mit etwas Phantasie kann man auch Daten auslesen oder löschen. Auf magic_quotes_gpc auf dem Server darf man sich übrigens nicht als Schutz verlassen: Die Einstellung gilt als überholt und soll in kommenden PHP-Versionen verschwinden. Außerdem schützt sie nur unvollständig. Gewöhnt euch lieber gleich die saubere Variante an.
Die Lösung: PDO
Seit PHP 5.1 gibt es PDO (PHP Data Objects). Das ist eine einheitliche Schnittstelle für verschiedene Datenbanken. Ein schöner Nebeneffekt: Wenn ihr später mal von MySQL auf etwas anderes wechselt, müsst ihr nicht den ganzen Code umschreiben. Und PDO kann Prepared Statements. Dabei wird die Abfrage mit Platzhaltern an die Datenbank geschickt, die Werte kommen separat. So kann nichts mehr als SQL-Befehl interpretiert werden, was eigentlich ein Wert sein soll.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=blog', 'benutzer', 'geheim');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("SET NAMES utf8");
$stmt = $pdo->prepare('SELECT * FROM benutzer WHERE name = :name AND pass = :pass');
$stmt->bindValue(':name', $_POST['name'], PDO::PARAM_STR);
$stmt->bindValue(':pass', md5($_POST['pass']), PDO::PARAM_STR);
$stmt->execute();
$benutzer = $stmt->fetch(PDO::FETCH_ASSOC);Mit ERRMODE_EXCEPTION bekommt ihr bei Fehlern eine Exception statt einer stillen Warnung, das macht die Fehlersuche deutlich angenehmer. Das SET NAMES utf8 sorgt dafür, dass Umlaute richtig ankommen. Wichtig ist, dass ihr die Eingaben nie selbst in den SQL-String schreibt, auch nicht „nur mal kurz zum Testen“. Aus solchen Provisorien wird erfahrungsgemäß Produktivcode.
bindParam oder bindValue?
Der Unterschied ist klein: bindValue übergibt den Wert sofort, bindParam bindet eine Variable als Referenz, die erst bei execute() ausgelesen wird. Das ist praktisch, wenn man dasselbe Statement in einer Schleife mehrfach ausführt:
<?php
$stmt = $pdo->prepare('INSERT INTO tags (name) VALUES (:name)');
$stmt->bindParam(':name', $tag);
foreach (array('php', 'mysql', 'pdo') as $tag) {
$stmt->execute();
}Am kürzesten geht es mit einem Array direkt in execute():
<?php
$stmt = $pdo->prepare('SELECT * FROM artikel WHERE id = ? AND kategorie = ?');
$stmt->execute(array((int) $_GET['id'], $_GET['kat']));Und wenn PDO nicht geht?
Manche Hoster haben die PDO-Erweiterung nicht aktiv. Dann bleibt als Notlösung mysql_real_escape_string:
<?php
$name = mysql_real_escape_string($_POST['name']);
$sql = "SELECT * FROM benutzer WHERE name = '" . $name . "'";Das funktioniert, solange man es wirklich überall daran denkt und die Werte in Anführungszeichen setzt. Ein einziges Mal vergessen reicht. Bei Prepared Statements kann man das gar nicht erst vergessen, deshalb ist PDO für mich die bessere Wahl.
Nutzt ihr PDO schon oder noch die alten mysql_-Funktionen? Schreibt es gern in die Kommentare!
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (PHP-Logo: Colin Viebrock; MySQL-Logo: Oracle Corporation), jeweils gemeinfrei; die Marken gehören ihren Inhabern.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.