Kurz gesagt: SQL-Injection in PHP verhindern Sie zuverlässig, indem Sie Werte nie in SQL-Texte einbauen, sondern immer über Prepared Statements mit Platzhaltern übergeben. Für Spalten- und Tabellennamen, die sich nicht als Platzhalter übergeben lassen, gilt eine feste Whitelist. Zusätzlich begrenzt ein Datenbankbenutzer mit minimalen Rechten den Schaden, falls doch eine Lücke bleibt.
Warum SQL-Injection nach PHP2Shell nicht aus dem Blick geraten darf
Im Oktober 2026 dreht sich vieles um PHP2Shell, zwei kritische Lücken im PHP-Interpreter, behoben seit dem 21.10.2026 in PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35. Viele Teams haben in diesen Tagen Server aktualisiert und Prozesse neu gestartet. Das ist richtig und dringend.
Ein aktueller PHP-Stand sagt aber nichts über den eigenen Code. SQL-Injection gehört seit Jahren zu den häufigsten Schwachstellen in Webanwendungen und wird in den OWASP Top 10 unter der Kategorie Injection geführt. Wer gerade ohnehin Sicherheitsprüfungen plant, sollte die Datenbankzugriffe der eigenen Anwendung gleich mit ansehen.
Die gute Nachricht: SQL-Injection ist eine der wenigen Schwachstellen, die sich mit einer einfachen, konsequent angewandten Regel fast vollständig ausschließen lässt.
Wie entsteht SQL-Injection in PHP?
SQL-Injection entsteht, wenn eine Anwendung Benutzereingaben als Teil eines SQL-Befehls behandelt statt als reine Daten. Typisch ist das Zusammensetzen einer Abfrage aus Textbausteinen und Variablen. Kann ein Besucher den Inhalt einer solchen Variable bestimmen, kann er unter Umständen die Bedeutung des Befehls verändern.
Die Folgen reichen vom Auslesen fremder Kundendaten über das Verändern von Preisen oder Rechten bis zum Löschen ganzer Tabellen. Je nach Datenbank und Rechten kann eine Lücke auch Zugriffe über die Datenbank hinaus ermöglichen. Sind personenbezogene Daten betroffen, kann eine Meldepflicht nach DSGVO entstehen.
Eingaben sind dabei nicht nur Formularfelder. Auch URL-Parameter, Cookies, HTTP-Header, hochgeladene Dateien, Antworten fremder Schnittstellen und bereits gespeicherte Daten aus der eigenen Datenbank gelten als nicht vertrauenswürdig.
Prepared Statements mit PDO und mysqli
Prepared Statements senden den SQL-Befehl und die Werte getrennt an die Datenbank. Die Werte werden nie als Teil des Befehls interpretiert. Mit PDO sieht eine sichere Abfrage so aus:
<?php
$pdo = new PDO($dsn, $benutzer, $passwort, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT id, name, email FROM kunden WHERE id = :id AND status = :status');
$stmt->execute([
'id' => (int) $_GET['id'],
'status' => 'aktiv',
]);
$kunde = $stmt->fetch();
Die Option PDO::ATTR_EMULATE_PREPARES => false sorgt dafür, dass die Datenbank echte Prepared Statements verwendet, soweit der Treiber das unterstützt. Mit mysqli gilt dasselbe Prinzip:
<?php
$stmt = $mysqli->prepare('SELECT id, name FROM produkte WHERE kategorie = ? AND preis <= ?');
$stmt->bind_param('sd', $kategorie, $maxPreis);
$stmt->execute();
$ergebnis = $stmt->get_result();
Auch Listen für IN (...) lassen sich sicher abbilden, indem Sie für jeden Wert einen eigenen Platzhalter erzeugen und die Werte getrennt übergeben.
<?php
$ids = array_map('intval', $ausgewaehlteIds);
$platzhalter = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, name FROM produkte WHERE id IN ($platzhalter)");
$stmt->execute($ids);
Sonderfälle: Spaltennamen, Sortierung und rohes SQL
Platzhalter funktionieren nur für Werte, nicht für Tabellennamen, Spaltennamen oder Sortierrichtungen. Genau hier entstehen in der Praxis viele Lücken, etwa bei sortierbaren Tabellen oder Filtern. Die Lösung ist eine feste Liste erlaubter Werte:
<?php
$erlaubteSpalten = ['name' => 'name', 'preis' => 'preis', 'datum' => 'erstellt_am'];
$spalte = $erlaubteSpalten[$_GET['sort'] ?? ''] ?? 'name';
$richtung = (($_GET['dir'] ?? '') === 'desc') ? 'DESC' : 'ASC';
$stmt = $pdo->query("SELECT id, name, preis FROM produkte ORDER BY $spalte $richtung");
Frameworks und CMS bringen eigene Werkzeuge mit. In WordPress übernimmt $wpdb->prepare() diese Aufgabe, in Laravel und Symfony die Query Builder und ORMs. Sobald Sie dort rohes SQL verwenden, etwa über Funktionen mit „raw“ im Namen, gelten wieder die Regeln dieses Abschnitts. Hinweise zu Laravel finden Sie unter Laravel-Anwendungen absichern.
Suchen Sie im Code gezielt nach SQL-Texten, in die Variablen eingesetzt werden. Ein einfacher Start ist eine Suche nach query( und ->raw in Kombination mit $_GET, $_POST oder $_REQUEST.
SQL-Injection im Bestandscode aufspüren
Bei neuer Software lässt sich die Regel von Anfang an durchhalten. Schwieriger ist es bei Anwendungen, die über Jahre gewachsen sind. Dort lohnt eine systematische Suche nach Stellen, an denen SQL-Texte mit Variablen zusammengesetzt werden:
# SQL-Befehle, in die Variablen eingesetzt werden
grep -rnE "(SELECT|INSERT|UPDATE|DELETE)[^;]*\\\$[a-zA-Z_]" src/ --include="*.php" | head -50
# Direkte Nutzung von Anfragedaten in Abfragefunktionen
grep -rnE "(query|exec|raw)\s*\(.*\\\$_(GET|POST|REQUEST|COOKIE)" src/ --include="*.php"
Nicht jeder Treffer ist eine Lücke, etwa wenn die Variable nur einen festen Tabellennamen enthält. Jede Fundstelle sollte aber einmal bewusst geprüft und bei Bedarf auf Prepared Statements umgestellt werden. Werkzeuge zur statischen Codeanalyse helfen, solche Muster dauerhaft in der Build-Pipeline zu erkennen, sodass neue Fehler gar nicht erst live gehen.
Schaden begrenzen: Rechte, Fehlermeldungen, Prüfung
Sicherer Code ist die erste Verteidigungslinie. Weitere Ebenen begrenzen den Schaden, falls doch eine Lücke übersehen wird:
- Minimale Rechte: Der Datenbankbenutzer der Website braucht meist nur
SELECT,INSERT,UPDATEundDELETEauf die eigene Datenbank, keine Rechte zum Anlegen von Benutzern oder zum Zugriff auf andere Datenbanken. - Keine Fehlermeldungen nach außen:
display_errors = Offin der php.ini. Datenbankfehler gehören ins Log, nicht in den Browser. - Trennung je Anwendung: Jede Website erhält einen eigenen Datenbankbenutzer.
- Abhängigkeiten aktuell halten: Lücken stecken auch in Bibliotheken, wie der Ratgeber composer audit zeigt.
- Prüfen lassen: Code-Review und automatisierte Analyse finden viele Stellen, ein Penetrationstest zeigt, was von außen tatsächlich erreichbar ist.
-- Beispiel: Datenbankbenutzer nur mit den nötigen Rechten
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY '<starkes Passwort>';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
Eine verwandte Schwachstelle betrifft die Ausgabe von Daten im Browser. Wie Sie diese absichern, erklärt der Ratgeber Cross-Site-Scripting in PHP verhindern.
Wie hilft PHP SOS konkret?
- Wir prüfen Datenbankzugriffe Ihrer Anwendung systematisch auf unsichere Abfragen.
- Wir priorisieren Funde nach Erreichbarkeit und betroffenen Daten und zeigen konkrete Korrekturen.
- Wir setzen Datenbankrechte und Fehlerbehandlung auf ein sicheres Niveau.
- Wir testen nach der Korrektur erneut und dokumentieren das Ergebnis.
Eine unabhängige Prüfung beschreibt die Leistung Sicherheitsaudit und Penetrationstest.
Häufige Fragen
Reicht es, Eingaben mit addslashes oder real_escape_string zu behandeln?
Nein, das ist fehleranfällig. Escaping hängt vom Zeichensatz und vom Kontext ab und wird leicht an einer Stelle vergessen. Prepared Statements trennen Befehl und Daten grundsätzlich und sind deshalb der empfohlene Weg.
Schützt ein ORM automatisch vor SQL-Injection?
Weitgehend, solange Sie die Abfragefunktionen des ORM nutzen. Sobald Sie rohes SQL schreiben oder Werte in Sortierungen und Spaltennamen einsetzen, gelten dieselben Regeln wie ohne ORM.
Hilft eine Web Application Firewall gegen SQL-Injection?
Eine WAF kann bekannte Muster abfangen und Zeit verschaffen. Sie ersetzt aber keinen sicheren Code, denn Regeln anhand von Anfragedaten lassen sich umgehen und treffen manchmal legitime Eingaben.
Schließt das PHP-Update gegen PHP2Shell auch SQL-Injection-Lücken?
Nein. PHP2Shell betrifft den PHP-Interpreter. SQL-Injection ist ein Fehler im Anwendungscode und bleibt bestehen, egal welche PHP-Version läuft.