Navigation
 Startseite
 Fachbücher
 Anzeigenmarkt
 Forum
 Webmaster News
 Script Newsletter
 Kontakt
 Script Installation
 Php
 Php Tutorials
 Lernpfade
 Webhoster Vergleich
 Impressum

Community-Bereich
 kostenlos Registrieren
 Anmelden
 Benutzerliste

Script Datenbank
 Script Archiv
 Script Top 20
 Screenshots
 Testberichte

Suche
 

Unsere Php Scripts
 Counter Script
 Umfrage Script
 Bilder Upload Script
 Terminverwaltung
 Simple PHP Forum
 RSS Grabber

Tools und Generatoren
 .htpasswd Generator
 md5 Generator
 base64 Generator
 Markdown to HTML
 Colorpicker
 Unix timestamp Tool
 Unit Test Generator
 TLD Liste
 Webkatalog‑Verzeichnis

Hosterplus.de
Bekommen Sie Speicherplatz (Webspace), Domains und...
https://www.Hosterplus.de
Artfiles.de
Bietet Serviceorientierte Internetdienstleistungen...
https://www.Artfiles.de
 
 
 

PHP SQL-Injection verhindern: Prepared Statements und Best Practices

Sie befinden sich: Home > Php Tutorial > PHP SQL-Injection verhinder...

PHP SQL-Injection verhindern: Prepared Statements und Best Practices
Eintrag am:
25.07.2026
Hits / Besucher:
25
Sprache:
  Deutsch
Tutorial Art:
eigenes
Eingetragen von:
Merkliste:
 
Beschreibung

Wer Formulardaten in eine Datenbankabfrage schreibt, arbeitet an der empfindlichsten Stelle einer Webanwendung. Ein einziger falsch zusammengebauter SQL-String reicht, um einem Angreifer Zugriff auf fremde Datensätze zu geben. Dieses Tutorial zeigt, wie so ein Angriff abläuft und mit welchen Mitteln PHP ihn zuverlässig ausschliesst. Wer in PHP SQL-Injection verhindern will, braucht dazu kein Spezialwissen, sondern wenige Patterns, die konsequent angewendet werden.

Was ist SQL-Injection?

SQL-Injection ist die Klassiker-Schwachstelle in jeder Webanwendung mit Datenbank. Ein Angreifer schiebt über ein Eingabefeld SQL-Code ein, den die Datenbank-Engine als Befehl interpretiert. Das Ergebnis reicht von ungewolltem Zugriff auf sensible Daten bis zum kompletten Verlust einer Tabelle. Wer in PHP SQL-Injection verhindern will, muss alle Eingaben aus Formularen als potenziell feindlich behandeln und die Schwachstelle erkennen, bevor er sie schließen kann.

Illustration zum Tutorial: PHP SQL-Injection verhindern: Prepared Statements und Best Practices

Bevor die Anatomie eines Angriffs und die Verteidigung mit Prepared Statements folgen, ein Blick auf die Bausteine, die zusammen für echten Schutz sorgen.

Die gute Nachricht: Der zentrale Schutz ist bekannt, robust und in PHP von Haus aus eingebaut. Prepared Statements trennen den SQL-Code vom Wert und nehmen damit dem Angreifer das wichtigste Werkzeug. Dieses Tutorial zeigt, wie ein klassischer Angriff aussieht, wie Prepared Statements mit PDO und mysqli aufgebaut sind und welche Patterns zusätzlich helfen, wenn sich nicht alles über Parameter binden lässt. Wer konsequent gegen Injection-Angriffe vorgehen will, kombiniert die folgenden Bausteine.

Anatomie eines Angriffs (' OR 1=1 -- )

Der häufigste Angriff zielt auf einen verwundbaren Login. Im folgenden Code wird der eingegebene Username direkt in die SQL-Anweisung eingebaut. Das ist der typische Anfängerfehler.

<?php

/* GEFAEHRLICHER Code - bitte NICHT verwenden */
$user = $_POST['username'];
$pass = $_POST['password'];
$sql = "SELECT id FROM users WHERE username = '$user' AND password = '$pass'";
$result = $mysqli->query($sql);

Sendet der Angreifer als Username den String ' OR 1=1 -- (mit Leerzeichen am Ende), wird daraus folgende Abfrage. Genau hier scheitert jeder Schutzversuch, der die Trennung von Code und Wert nicht konsequent durchzieht.

SELECT id FROM users

WHERE username = '' OR 1=1 -- ' AND password = ''

Das -- kommentiert den Rest der Zeile aus, OR 1=1 macht die Bedingung dauerhaft wahr, und der Login-Code findet eine ID. Genau hier setzt der Schutz an: Der String ' OR 1=1 -- darf niemals als SQL-Code interpretiert werden, sondern muss als reiner Eingabewert behandelt werden.

Schutz Schritt 1: Prepared Statements mit PDO

Prepared Statements drehen den Spieß um und sind das wichtigste Werkzeug gegen Injection-Angriffe. Zuerst wird die Vorlage mit Platzhaltern an die Datenbank geschickt, dann werden die Werte separat gebunden. Die Engine weiß damit klar: Hier ist Code, dort sind Werte.

<?php

$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);

$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE username = :u');
$stmt->execute([':u' => $_POST['username']]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if ($row !== false && password_verify($_POST['password'], $row['password_hash'])) {
/* Login ok */
}

Die Option ATTR_EMULATE_PREPARES = false ist wichtig, weil sie sicherstellt, dass echte Server-Prepared-Statements verwendet werden. Damit greifen alle Schutzgarantien zuverlässig. Wer tiefer in die PDO-Welt einsteigen möchte, findet im Tutorial PHP PDO sichere Datenbankzugriffe eine ausführliche Vertiefung.

Schutz Schritt 2: Prepared Statements mit mysqli

Wer mysqli statt PDO einsetzt, hat dieselbe Schutzwirkung. Die Syntax unterscheidet sich, das Prinzip bleibt gleich: erst Vorlage, dann Werte binden.

<?php

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'user', 'pass', 'app');
$mysqli->set_charset('utf8mb4');

$stmt = $mysqli->prepare('SELECT id, password_hash FROM users WHERE username = ?');
$stmt->bind_param('s', $username);
$username = $_POST['username'];
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$stmt->close();

Eine vertiefende Vorstellung zu Prepared Statements und beiden APIs findet sich im Tutorial zu PDO und sicheren Datenbankzugriffen. In der Schutzwirkung sind PDO und mysqli gleichwertig, solange Prepared Statements konsequent eingesetzt werden. Wer beides verstanden hat, ist gegen Injection-Angriffe gewappnet, unabhängig davon, welche Erweiterung im Projekt liegt.

bind_param Type-Spezifikatoren bei mysqli und PDO

Bei mysqli erwartet bind_param() als ersten Parameter einen String mit Typ-Codes, die den Wert-Typ festlegen. Diese vier Buchstaben sind Pflichtwissen, weil sie steuern, wie der Wert an die Datenbank geschickt wird.

CodeBedeutungBeispiel-Werte
sstring'test', '2026-05-16', beliebiger Text
iinteger42, (int) $_POST['id']
ddouble / float19.95, 0.5
bblob (binary)große Binärdaten via send_long_data()

Wer den Code verwechselt, riskiert Daten-Verlust oder Conversion-Fehler. Ein als s übergebener Integer ist meist unproblematisch, ein als i übergebener Wert mit führender Null verliert die Null. Bei PDO ist die analoge Einstellung PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_BOOL und PDO::PARAM_LOB.

<?php

/* mysqli mit allen vier Typen kombiniert */
$stmt = $mysqli->prepare('
INSERT INTO produkte (name, kategorie_id, preis, foto)
VALUES (?, ?, ?, ?)
');
$stmt->bind_param('sids', $name, $kategorieId, $preis, $foto);
$stmt->execute();

/* PDO mit explizitem Typ pro Parameter */
$stmt = $pdo->prepare('SELECT * FROM produkte WHERE id = :id AND aktiv = :aktiv');
$stmt->bindValue(':id', (int) $_GET['id'], PDO::PARAM_INT);
$stmt->bindValue(':aktiv', true, PDO::PARAM_BOOL);
$stmt->execute();

Die explizite Typangabe ist nicht nur Doku, sie ist sicherheitsrelevant. Mit PDO::ATTR_EMULATE_PREPARES = false werden die Werte als typisierte Parameter an die Datenbank geschickt und sind dadurch garantiert vom SQL-Code getrennt.

LIMIT-Klausel mit Integer-Bindings: die häufigste Stolperfalle

Eine berüchtigte Falle bei PDO ist LIMIT ?, ?. Ohne explizites PARAM_INT werden die Werte als String gebunden, MySQL beschwert sich mit Syntax-Fehler, der scheinbar aus dem Nichts kommt. Der Grund: bei aktivierten emulierten Prepares (Default in älteren PHP-Versionen) wird der String mit Anführungszeichen eingesetzt, und LIMIT '0', '20' ist kein gültiges SQL.

<?php

/* Falsch: bricht mit "near ''0', '20''" ab */
$stmt = $pdo->prepare('SELECT * FROM artikel ORDER BY id LIMIT ?, ?');
$stmt->execute([0, 20]);

/* Richtig Variante 1: ATTR_EMULATE_PREPARES auf false */
$pdo = new PDO($dsn, $u, $p, [PDO::ATTR_EMULATE_PREPARES => false]);
$stmt = $pdo->prepare('SELECT * FROM artikel ORDER BY id LIMIT ?, ?');
$stmt->execute([0, 20]);

/* Richtig Variante 2: explizite Bindings mit PARAM_INT */
$stmt = $pdo->prepare('SELECT * FROM artikel ORDER BY id LIMIT :off, :len');
$stmt->bindValue(':off', (int) $_GET['offset'], PDO::PARAM_INT);
$stmt->bindValue(':len', 20, PDO::PARAM_INT);
$stmt->execute();

Wichtig: trotz Prepared Statement gilt auch hier (int)-Cast als zweite Verteidigungslinie. Wenn der Offset ohnehin in einen Integer gecastet wird, kann nichts mehr daneben gehen, selbst wenn jemand später aus Versehen die Emulation wieder einschaltet.

Schutz Schritt 3: Whitelist für dynamische Spalten

Prepared Statements funktionieren ausschliesslich für Werte. Tabellen- oder Spaltennamen können nicht über Platzhalter gebunden werden. Wer eine sortierbare Liste oder dynamische Filter baut, muss die Identifier deshalb selbst kontrollieren. Bewährt ist das Whitelist-Pattern.

<?php

$erlaubt = ['name', 'erstellt_am', 'preis'];
$sortBy = in_array($_GET['sort'] ?? '', $erlaubt, true) ? $_GET['sort'] : 'name';
$richtung = ($_GET['dir'] ?? '') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT id, name, preis FROM artikel ORDER BY $sortBy $richtung";
$result = $pdo->query($sql);

Der Trick: $sortBy kann nur einen der drei vordefinierten Werte annehmen, alles andere wird auf den Default name gemappt. Der Angreifer hat keine Chance, einen eigenen String einzuschmuggeln, weil der Wert vor dem SQL-Bau strikt validiert wird. Das gleiche Prinzip gilt für dynamische Tabellennamen, Limit-Werte oder Direction-Strings.

Wann pdo_quote oder Escape-Funktionen sinnvoll sind

Bevor es Prepared Statements gab, wurden Werte mit mysql_real_escape_string() und später mysqli_real_escape_string() maskiert. Diese Funktionen existieren noch, sind aber aus heutiger Sicht ein Notnagel. Sie sollten nur dann zum Einsatz kommen, wenn ein Prepared Statement aus einem konkreten Grund nicht möglich ist.

<?php

/* nur als letzter Notnagel */
$safe = $pdo->quote($_GET['kategorie']);
$sql = "SELECT id FROM artikel WHERE kategorie = $safe";
$result = $pdo->query($sql);

Wichtig: Escape-Funktionen schützen nicht vor Identifier-Injektion (Tabellen- und Spaltennamen). Und sie helfen nichts, wenn das Charset falsch konfiguriert ist. Die einfachere und sicherere Antwort lautet fast immer: Prepared Statement plus Whitelist. Eine besonders häufige Verwechslung: htmlspecialchars() schützt vor XSS-Angriffen, aber nicht vor SQL-Injection. Beides sind unterschiedliche Layer.

ORM und Stored Procedures als zusätzliche Schichten

Wer ein ORM wie Doctrine oder Eloquent einsetzt, profitiert von einer weiteren Schutzschicht. Beide Bibliotheken bauen ihre SQL-Statements intern selbst und nutzen für Werte konsequent Prepared Statements. Manuelle Konkatenation ist nur dort möglich, wo der Entwickler bewusst rohes SQL schreibt (etwa per DB::raw() in Laravel oder createNativeQuery() in Doctrine). Der typische ORM-Alltag besteht aus Builder-Aufrufen, die SQL-Injection schon strukturell ausschliessen.

<?php

/* Eloquent (Laravel): Werte werden automatisch als Prepared-Param gebunden */
$user = User::where('email', $_POST['email'])->first();

/* Doctrine QueryBuilder: gleiches Prinzip mit setParameter */
$qb = $em->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->where('u.email = :email')
->setParameter('email', $_POST['email']);
$user = $qb->getQuery()->getOneOrNullResult();

Stored Procedures sind die Variante auf DB-Seite. Die SQL-Logik wandert in eine in der Datenbank gespeicherte Prozedur, der PHP-Code ruft sie nur noch mit Parametern auf. Stored Procedures schützen nicht automatisch vor SQL-Injection, sondern nur dann, wenn die Aufruf-Parameter selbst per Prepared Statement gebunden werden. Dynamisches SQL innerhalb einer Stored Procedure (PREPARE stmt FROM @sql) bringt das Problem direkt in die DB.

<?php

/* Stored Procedure aufrufen, Parameter wie immer per Prepared Statement */
$stmt = $pdo->prepare('CALL get_user_by_email(?)');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

In typischen Web-Setups bleibt das Standard-Werkzeug ein Prepared Statement im PHP-Code. Stored Procedures sind sinnvoll bei komplexer Business-Logik, die nahe an den Daten laufen muss, oder bei strikten DB-Berechtigungs-Modellen, in denen der Webserver-User nur EXECUTE-Rechte auf bestimmte Prozeduren bekommt.

UTF-8-Charset im DSN als Sicherheits-Detail

Ein oft übersehener Aspekt: das Charset im PDO-DSN. Vor PHP 5.3.6 gab es Bypass-Angriffe, bei denen ein falsch gesetztes Charset eine bestimmte Multibyte-Sequenz so interpretierte, dass das Escaping ausgehebelt wurde. Die Lücke war nur bei SET NAMES-Anweisungen aktiv, die das Charset nachträglich umstellen. Heute ist die Regel klar: Charset im DSN angeben, niemals nachträglich per SET NAMES.

<?php

/* Richtig: charset im DSN, kein nachtraegliches SET NAMES noetig */
$pdo = new PDO(
'mysql:host=localhost;dbname=app;charset=utf8mb4',
'user', 'pass',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);

/* Bei mysqli analog: set_charset direkt nach Verbindungsaufbau */
$mysqli->set_charset('utf8mb4');

utf8mb4 ist die korrekte Wahl für echte UTF-8-Unterstützung mit Emoji und vollem 4-Byte-Bereich. Das alte utf8 von MySQL ist nur ein Drei-Byte-Subset und für moderne Anwendungen ungeeignet. Wer das Charset im DSN setzt, braucht im Normalbetrieb keinen SET NAMES-Aufruf mehr und hat damit den historischen Bypass-Vektor automatisch ausgeschlossen.

Error-Handling bei Prepared Statements

Was passiert, wenn ein Prepared Statement scheitert? Im Default schweigt PDO und liefert false zurück, was viele Skripte ignorieren. Mit PDO::ERRMODE_EXCEPTION werden Fehler als PDOException geworfen und lassen sich sauber abfangen.

<?php

try {
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
$row = $stmt->fetch();
} catch (PDOException $e) {
/* Niemals den Original-Fehler an den User ausgeben - er kann
* Tabellen-Namen, Spalten-Namen oder Stack-Traces enthalten,
* die einem Angreifer die DB-Struktur verraten. */
error_log('DB-Fehler: ' . $e->getMessage());
http_response_code(500);
exit('Interner Serverfehler.');
}

Diese Trennung ist sicherheitsrelevant: der echte Fehler wandert in das Server-Log, der Nutzer sieht nur eine generische Meldung. Display-Errors auf einem Production-System ist ohnehin ein No-Go (display_errors = Off in der php.ini), aber selbst mit korrekter Konfig ist try/catch um DB-Aufrufe ein guter Reflex.

Best Practices und Code-Review-Marker

Wer in PHP SQL-Injection verhindern will, schafft das nicht mit einer einzelnen Funktion, sondern mit einer Liste konsequent angewandter Praktiken. Die folgenden Marker helfen im Code-Review, Risikofälle zu erkennen.

<?php

/*
* Verdaechtige Muster im Code-Review:
*
* 1. SQL-String aus $_GET, $_POST oder $_REQUEST konkateniert
* 2. Variablen direkt in Spalten- oder Tabellennamen
* 3. mysqli_query($conn, $sql) mit dynamischen Anteilen
* 4. PDO->query($sql) statt PDO->prepare()
* 5. Stacked Queries via ATTR_EMULATE_PREPARES = true
*/

Ergänzend gilt das Least-Privilege-Prinzip: Der DB-User der Webanwendung sollte keine ALTER-, DROP- oder GRANT-Rechte besitzen, sondern nur die nötigsten DML-Operationen. Auch eine Web Application Firewall (WAF) kann typische SQL-Injection-Muster blockieren, ersetzt aber niemals die richtige Implementierung.

Wer ein Login-System baut, kombiniert Prepared Statements mit password_hash und password_verify und session_regenerate_id zu einem konsistenten Sicherheits-Paket. Erst diese Kombination macht aus einer Anfängeranwendung eine produktionsreife Lösung.

Der Unterschied zwischen beiden Wegen lässt sich in einem kurzen Ablauf zusammenfassen.


flowchart TD
    A[User-Eingabe] --> B{Konkatenierter SQL?}
    B -->|Ja| C[Angreifer schmuggelt SQL ein]
    C --> D[DB fuehrt fremden Code aus]
    B -->|Nein| E[Prepared Statement bindet Wert]
    E --> F[Wert wird nie als Code gelesen]

Fazit zum Thema PHP SQL-Injection verhindern

In PHP SQL-Injection verhindern ist kein Hexenwerk, wenn ein paar wenige Patterns konsequent eingehalten werden. Prepared Statements mit PDO oder mysqli sind das zentrale Werkzeug und decken den Großteil aller Fälle ab. Für dynamische Identifier wie Spalten- oder Tabellennamen kommt das Whitelist-Pattern dazu. Escape-Funktionen sind heute der Notnagel, nicht das Standardwerkzeug. Wer zusätzlich auf Least Privilege und sichere Begleit-Layer wie HttpOnly-Cookies und CSRF-Token setzt, hat eine Anwendung, die einem realen Angriff stand hält. Damit wird die Aufgabe weniger eine Frage der Technik als eine Frage der Disziplin im Code-Review.

 

Tags:

 

 

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Melde dich an, um einen Kommentar zu schreiben.
Bücherregal mit drei Büchern: 'PHP 4 - Grundlagen und Profiwissen' von Hanser Verlag, 'Webdesign in a Nutshell' von O'Reilly Verlag, und 'Webgestaltung' von Galileo Computing.