SQL und Eingabedaten sicher trennen
Voraussetzungen und Lernziele
Du kannst einfache PHP-Funktionen und eine SQL-SELECT-Abfrage lesen. Nach dieser Lektion kannst du erklären, weshalb Eingabedaten nicht als SQL-Struktur behandelt werden dürfen, eine parametrisierte PDO-Abfrage schreiben und die Grenzen von Platzhaltern benennen. Plane 120 Minuten ein: 30 Minuten Lesen, 60 Minuten Übungen, 30 Minuten Transfer und Reflexion.
Das eigentliche Problem: Daten werden zu Befehlen
Eine Anwendung erhält eine E-Mail-Adresse aus einem Formular und sucht das zugehörige Konto. Das Eingabefeld liefert eine Zeichenfolge. Ob sie als Adresse sinnvoll ist, weiss die Datenbank zunächst nicht. Problematisch wird es, wenn die Anwendung diese Zeichenfolge in den Text einer SQL-Anweisung einsetzt:
// Unsicher – ausschliesslich als Negativbeispiel lesen.
$sql = "SELECT id FROM users WHERE email = '" . $email . "'";
Hier bestimmt dieselbe Zeichenfolge sowohl einen Datenwert als auch möglicherweise Teile der Abfragesyntax. Ein enthaltenes Anführungszeichen kann den vorgesehenen Wertebereich verlassen. Der Fehler liegt nicht darin, dass ein Mensch ein bestimmtes Sonderzeichen tippt. Er liegt darin, dass die Anwendung die Grenze zwischen Sprache und Daten aufgibt.
Eine Eingabevalidierung wie «muss eine E-Mail-Adresse sein» ist trotzdem sinnvoll: Sie verbessert Datenqualität und Fehlermeldungen. Sie ersetzt aber keine sichere Datenbankschnittstelle. Später kann dieselbe Abfrage von einem Import, einem Hintergrundjob oder einer anderen Formularvariante aufgerufen werden.
Die Grenze ausdrücklich ziehen
PDO stellt vorbereitete Anweisungen bereit. Die SQL-Struktur enthält Platzhalter; Werte werden separat übergeben:
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->prepare(
'SELECT id, display_name FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
Die Zugangsdaten stammen aus einer geschützten Konfiguration, nicht aus einem Repository. Das Beispiel lässt ihre Einrichtung bewusst ausserhalb des Codes. Der Treiber übernimmt die korrekte Übergabe des Werts. Ein Apostroph gehört dann zum Wert, statt von uns in die Abfragesprache eingebaut zu werden. Ob der Treiber eine native Vorbereitung oder eine Emulation verwendet, hängt von Treiber und Konfiguration ab. Prüfe diese Eigenschaften für dein konkretes Datenbanksystem; verlasse dich nicht auf eine pauschale Aussage über alle PDO-Treiber.
Der Datenfluss lautet:
Formular → Validierung → Datenwert ───────────┐
↓
Feste SQL-Struktur → prepare() → execute(Werte) → Ergebnis
Ein leeres Ergebnis ist kein Serverfehler. Das Konto wurde möglicherweise nicht gefunden. Eine Datenbankexception dagegen kann auf Verbindungs-, Schema- oder Abfrageprobleme hinweisen. Zeige Besuchenden keine internen Verbindungsdaten oder vollständigen Stacktraces. Protokolliere nur, was für die Fehlersuche erforderlich ist, und vermeide personenbezogene Suchwerte im Log.
Was ein Platzhalter nicht kann
Ein Platzhalter steht für einen vollständigen Datenwert. Er ist kein allgemeiner Textersetzungsmechanismus. Insbesondere wählst du damit nicht beliebig eine Tabelle, eine Spalte oder die Richtung einer Sortierung aus.
Für eine erlaubte Sortierung brauchst du eine feste Zuordnung:
$columns = ['name' => 'display_name', 'created' => 'created_at'];
$column = $columns[$sortKey] ?? 'display_name';
$sql = 'SELECT id, display_name FROM users ORDER BY ' . $column;
$stmt = $pdo->query($sql);
Hier kommt die eingesetzte Spalte ausschliesslich aus dem von uns festgelegten Array. Niemals wird ein unbekannter Schlüssel selbst als SQL übernommen. Eine solche Positivliste ist überprüfbar. Für dynamische Listen in einer IN-Bedingung sind entsprechend viele einzelne Platzhalter erforderlich; ein einzelner Platzhalter ist keine Liste von SQL-Ausdrücken.
Typische Fehler
- Platzhalter in Anführungszeichen setzen:
WHERE email = ':email'bezeichnet einen SQL-Textwert und nicht den vorgesehenen Parameter. - SQL-Fragmente aus Eingaben zusammensetzen: Auch eine überwiegend parametrisierte Abfrage bleibt unsicher, wenn ein anderer Bestandteil ungeprüft eingefügt wird.
- Authentifizierung mit Autorisierung verwechseln: Eine sichere Suche nach einer Rechnung beantwortet noch nicht, ob die angemeldete Person diese Rechnung sehen darf.
- «Escaping erledigt alles»: Die korrekte Behandlung hängt vom Kontext ab. HTML-Ausgabe benötigt einen anderen Schutz als SQL.
Verständnisfragen
- Weshalb bleibt die Parametrisierung nötig, obwohl das Formular eine gültige Adresse verlangt?
- Warum ist ein leerer Treffer nicht dasselbe wie eine fehlgeschlagene Datenbankverbindung?
- Was ist an
ORDER BY :columnals gewünschter dynamischer Spaltenwahl falsch?
Erklärungen
- Validierung prüft fachliche Erwartungen. Die Trennung von SQL-Struktur und Werten muss auch bei anderen Aufrufwegen erhalten bleiben.
- Die Suche kann erfolgreich ausgeführt worden sein, ohne passende Daten zu finden. Verbindungsfehler verhindern die Ausführung.
- Ein Datenparameter ersetzt keinen Bezeichner. Eine feste Zuordnung erlaubter Spalten löst ein anderes Problem als ein Werteparameter.
Übung: Eine fremde Rechnung verhindern
Eine Anwendung soll eine Rechnung anhand ihrer Nummer laden. invoices besitzt die Spalten id, owner_id und amount_cents. Die aktuelle Person hat die serverseitig aus ihrer Sitzung ermittelte Kennung $currentUserId.
Schreibe eine Abfrage, die nur die angeforderte Rechnung dieser Person zurückgibt. Überlege, warum eine versteckte Formularangabe owner_id nicht als Nachweis der Identität genügt. Prüfe lokal mit eigenen Testdaten: eigene Rechnung, fremde Rechnung, unbekannte Nummer und eine Eingabe mit Sonderzeichen. Führe die Tests ausschliesslich gegen deine eigene Übungsdatenbank aus.
Hilfe 1
Du benötigst zwei Bedingungen in derselben WHERE-Klausel. Beide Werte werden separat gebunden.
Hilfe 2 und Musterlösung
$stmt = $pdo->prepare(
'SELECT id, amount_cents FROM invoices'
. ' WHERE id = :invoice AND owner_id = :owner'
);
$stmt->execute([
'invoice' => $invoiceId,
'owner' => $currentUserId,
]);
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
Die Rechnung ist nur auffindbar, wenn beide Bedingungen passen. Die Eigentümerkennung muss aus der vertrauenswürdig geprüften Sitzung stammen. Die versteckte Formularangabe kann von Besuchenden geändert werden. Die Lösung kombiniert eine parametrisierte Abfrage mit einer objektbezogenen Berechtigungsbedingung. Andere Aktionen wie Ändern oder Herunterladen müssen dieselbe Prüfung ebenfalls durchsetzen.
Transfer und Selbstkontrolle
Entwirf einen Testplan für eine Suche mit frei wählbarer Sortierung. Benenne gültige Schlüssel, unbekannte Schlüssel, leere Eingaben und den erwarteten Standard. Begründe, welche Testfälle Datenqualität, welche Abfragesicherheit und welche Zugriffsrechte prüfen. Stelle anschliessend die Frage: Würde ein zusätzlicher API-Endpunkt dieselben Regeln einhalten?
Zusammenfassung
Eine vorbereitete Anweisung hält feste Abfragestruktur und Datenwerte auseinander. Fachliche Validierung, Autorisierung und sichere Fehlerbehandlung bleiben zusätzliche Aufgaben. Gute Sicherheit entsteht aus überprüfbaren Grenzen zwischen diesen Verantwortlichkeiten, nicht aus einem einzelnen Filter.
Quellen und Prüfung
Primärquellen zuletzt inhaltlich konsultiert am 8. November 2026. Versionsangaben beachten; eine Dokumentationsseite kann sich später ändern.
Konto erstellen, um deinen Bearbeitungsstand zu speichern.