aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Webentwicklung

SQL-Injection: was es ist und wie man es verhindert

SQL-Injection (SQL injection) ist einer der ältesten und verheerendsten Angriffe auf Webanwendungen; sie entsteht, wenn Daten vom Benutzer direkt in eine SQL-Abfrage gemischt werden. Indem ein Angreifer Text sendet, der die Bedeutung der Abfrage verändert statt eines normalen Werts, kann er Daten lesen, ändern oder sogar ganze Tabellen löschen. Die gute Nachricht: Mit den richtigen Mustern lässt sich diese gesamte Angriffsklasse fast vollständig schließen. In diesem Artikel sehen wir anhand echter Beispiele, wie sie funktioniert, und bauen Schritt für Schritt die wichtigste Verteidigungslinie auf — Prepared Statements (parametrisierte Abfragen).

Wie funktioniert SQL-Injection?

Das Kernproblem ist, dass der Code Daten und Befehl in derselben Zeichenkette zusammenfügt. Die Datenbank interpretiert den gesamten Text, den sie erhält; sie hat keine Möglichkeit zu wissen, welcher Teil „vertrauenswürdiger Code" und welcher „Benutzerdaten" sind. Ein klassisches, fehlerhaftes Beispiel:

// MACH DAS NIEMALS
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $db->query($sql);

Tippt der Benutzer eine normale Adresse ein, gibt es kein Problem. Ist die Eingabe aber ' OR '1'='1, wird die Abfrage zu:

SELECT * FROM users WHERE email = '' OR '1'='1'

Da '1'='1' immer wahr ist, gibt die Abfrage jeden Benutzer zurück. Auf einem Login-Bildschirm bedeutet das, sich ohne Passwort anzumelden.

Echte Angriffsbeispiele

SQL-Injection kommt nicht in einer einzigen Form vor. Die häufigsten Typen zu erkennen erklärt, warum einfache Tricks wie „Anführungszeichen entfernen" nicht ausreichen.

  • Umgehung der Authentifizierung: Das Muster OR '1'='1 oben ist das bekannteste Beispiel auf Login-Formularen.
  • UNION-basiert: Der Angreifer hängt sein eigenes SELECT-Ergebnis an die bestehende Abfrage an. Mit ' UNION SELECT username, password FROM users -- lassen sich etwa Daten aus einer anderen Tabelle abgreifen.
  • Fehlerbasiert (error-based): Detaillierte Fehlermeldungen der Datenbank legen die Tabellen- und Spaltenstruktur offen; der Angreifer nutzt sie, um das Schema zu kartieren.
  • Blinde (blind) Injection: Selbst wenn die Anwendung keine Fehler anzeigt, lassen sich Daten Bit für Bit extrahieren, indem man wahr/falsch-Antworten oder die Antwortzeit beobachtet (wie AND SLEEP(5)).

Ein wichtiger Punkt: Zu versuchen, die Eingabe durch Entfernen von Anführungszeichen zu „säubern", ist eine brüchige Verteidigung. Verschiedene Datenbanken haben unterschiedliche Escaping-Regeln und Codierungstricks; früher oder später bleibt eine Lücke. Die richtige Lösung ist, Daten vollständig vom Code zu trennen.

Die wichtigste Verteidigung: Prepared Statements

Ein Prepared Statement (parametrisierte Abfrage) trennt die Struktur der Abfrage von ihren Daten. Zuerst bereitest du die Abfragevorlage mit Platzhaltern vor, dann sendest du die Werte über einen separaten Kanal. Was auch immer der Wert ist, die Datenbank behandelt ihn als reinen Wert, niemals als Befehl. In PHP mit PDO:

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

Hier ist ? ein Platzhalter. Der Wert, den du an execute() übergibst, wird eingefügt, nachdem die Abfragestruktur bereits gesperrt ist; ob er ein Anführungszeichen, ein OR oder ein -- enthält, ändert nichts. Benannte Parameter verbessern die Lesbarkeit:

$stmt = $pdo->prepare(
    'INSERT INTO posts (title, user_id) VALUES (:title, :user_id)'
);
$stmt->execute([':title' => $title, ':user_id' => $userId]);

Wenn du PDO verwendest, schalte die Emulation aus, um sicherzustellen, dass der Treiber echte Prepared Statements nutzt:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Dasselbe Prinzip gilt in jeder Sprache. In Node.js mit mysql2:

const [rows] = await conn.execute(
  'SELECT id, name FROM users WHERE email = ?',
  [email]
);

Es gibt eine einzige Schlüsselregel: Klebe Benutzerdaten niemals per String-Verkettung in eine Abfrage. Übergib sie immer als Parameter.

ORMs und Query Builder

Moderne Frameworks erzeugen bei korrekter Verwendung automatisch parametrisierte Abfragen. In Laravel binden Eloquent und der Query Builder Werte von selbst als Parameter:

// Sicher — Wert wird automatisch gebunden
User::where('email', $email)->first();

DB::table('users')->where('email', $email)->get();

Aber ein ORM schützt dich nicht automatisch; sobald du rohes SQL schreibst, geht die Verantwortung wieder an dich über. Setzt du Benutzerdaten direkt in DB::raw() oder ein whereRaw()-Argument, entsteht die Lücke erneut. Verwende auch dort Bindings:

// Falsch: Wert direkt eingebettet
DB::select("SELECT * FROM users WHERE email = '$email'");

// Richtig: Bindings-Array
DB::select('SELECT * FROM users WHERE email = ?', [$email]);

Wie hältst du Tabellen- und Spaltennamen sicher?

Platzhalter funktionieren nur für Werte; strukturelle Teile wie ein Tabellenname, ein Spaltenname oder eine ORDER BY-Richtung können keine Parameter sein. ORDER BY ? funktioniert nicht. Wenn ein Benutzer wählen kann, nach welcher Spalte sortiert wird, setze den eingehenden Wert nicht direkt in die Abfrage — validiere ihn gegen eine feste Whitelist:

$allowed = ['name', 'created_at', 'price'];
$column = in_array($_GET['sort'], $allowed, true)
    ? $_GET['sort']
    : 'name';

$sql = "SELECT * FROM products ORDER BY $column";

Hier kann der Benutzer nur einen der vorab genehmigten Werte wählen; kein anderer Text kann die Abfrage erreichen.

Verteidigung in der Tiefe

Prepared Statements sind die wichtigste und ausreichende Verteidigung gegen SQL-Injection. Aber Sicherheit ist ganzheitlich; diese Schichten verkleinern die Angriffsfläche und begrenzen möglichen Schaden:

  • Prinzip der geringsten Rechte: Gib dem Datenbankbenutzer der Anwendung nur die Berechtigungen, die er braucht. Eine Web-App sollte keine DROP TABLE- oder GRANT-Rechte haben.
  • Eingabevalidierung: Erzwinge den erwarteten Typ und das Format (eine E-Mail sieht aus wie eine E-Mail, eine Zahl wie eine Zahl). Das allein verhindert keine Injection, verkleinert aber die Angriffsfläche.
  • Fehlermeldungen verbergen: Zeige in der Produktion niemals rohe Datenbankfehler dem Benutzer; protokolliere sie und gib eine allgemeine Meldung zurück. Sonst lädst du fehlerbasierte Injection ein.
  • WAF und Monitoring: Eine Web Application Firewall kann bekannte Angriffsmuster filtern; allein nicht ausreichend, aber eine zusätzliche Schicht.

Häufige Fragen

Verhindert das Bereinigen (Sanitizing) der Eingabe SQL-Injection?

Allein nicht zuverlässig. Das Escapen von Anführungszeichen und Blacklist-Filter lassen sich auf verschiedenen Datenbanken und über Codierungstricks umgehen. Die echte Lösung sind parametrisierte Abfragen; das Bereinigen ist nur eine ergänzende Maßnahme.

Ich nutze ein ORM — besteht trotzdem ein Risiko?

Bei Standardabfragen schützt dich das ORM, weil es Werte automatisch bindet. Das Risiko entsteht dort, wo du rohes SQL schreibst (whereRaw, DB::raw, rohe Verkettung). Verwende dort immer Binding-Parameter.

Reicht eine Stored Procedure aus?

Allein nicht. Baust du innerhalb einer Stored Procedure dynamisches SQL per String-Verkettung auf, wandert die Schwachstelle einfach in die Prozedur. Du musst denselben parametrisierten Ansatz auch innerhalb von Prozeduren befolgen.

Überlass die Sicherheit deiner Anwendung nicht dem Zufall. Wenn ich eine bestehende Codebasis auf SQL-Injection prüfen oder eine sichere Datenschicht aufbauen soll, nimm Kontakt auf und lass uns gemeinsam ein solides Fundament legen.

Bu kategorideki tüm yazılar →

Devamı için