SQL-Injection und Parameter
Kurz gesagt: SQL-Injection ist ein Angriff, bei dem eine in die Abfrage eingefügte Benutzereingabe als SQL läuft, und eine parametrisierte Abfrage (Prepared Statement) verhindert ihn.
Problem
Das Suchfeld im Admin-Bereich
Der Admin-Bereich hat ein Feld „E-Mail-Adresse des Kunden“. Das Programm nimmt, was getippt wurde, und setzt es in die Abfrage:
SELECT name, email, city FROM customers WHERE email = '<was getippt wurde>'Mit maria.ivanova@example.com bekommst du Maria. Aber getippt werden kann jeder Text, auch ein Anführungszeichen. Dann ist es kein Wert mehr, sondern wird Teil der Abfrage. Das heißt SQL-Injection: Der Besucher schreibt SQL an die Stelle des Programms.
Probier es aus
Alle Kunden anzeigen
Probier es selbst. Das Ziel: Die Seite soll alle Kunden anzeigen, ohne dass du eine einzige E-Mail-Adresse kennst. Achte auf die Zeile „Was bei der Datenbank ankommt“: Genau dieser Text wird von der Datenbank ausgeführt.
Was bei der Datenbank ankommt
Genauer, für Neugierige
Ein Tipp: Ein Anführungszeichen schließt den Text, und danach brauchst du eine Bedingung, die für jede Zeile wahr ist. Zum Beispiel x' OR '1'='1. Das letzte Anführungszeichen lässt du weg, weil das Programm es selbst anhängt.
Sieh es dir an
Zwei weitere Tricks
Die Eingabe muss keine raffinierte Bedingung sein. Der Kommentar -- entfernt alles, was danach kommt, auch eine Prüfung, die das Programm hinter die E-Mail-Adresse gesetzt hat:
Das Programm sucht einen Kunden mit dieser E-Mail-Adresse aus Varna, und heraus kommt Maria aus Sofia: Die Prüfung der Stadt ist im Kommentar gelandet.
Wenn der Treiber mehrere Anweisungen aus einem Text ausführt, fügt die Eingabe x'; DELETE FROM customers; -- eine ganz neue Anweisung hinzu. Probier es im Feld oben aus (danach „Neue Datenbank“). In der echten Welt löscht so eine Eingabe Tabellen, ändert Passwörter oder liest Daten, die sie nie sehen dürfte.
Die Idee
Dasselbe im Code: vier Treiber, vier Verhaltensweisen
Hier ist die Suche mit eingeklebtem Text in allen vier Sprachen. Das Programm sucht nach drei Dingen: einer echten E-Mail-Adresse, x' OR '1'='1 und x'; DELETE FROM customers; --, und zählt am Ende die Kunden:
static void find(Connection conn, String email) throws SQLException { String sql = "SELECT name FROM customers WHERE email = '" + email + "'"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { List<String> names = new ArrayList<>(); while (rs.next()) { names.add(rs.getString("name")); } System.out.println(email + " → " + names); }}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); find(conn, "maria.ivanova@example.com"); find(conn, "x' OR '1'='1"); try { find(conn, "x'; DELETE FROM customers; --"); } catch (SQLException e) { System.out.println("Fehler: " + e.getMessage()); } try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) { rs.next(); System.out.println("Kunden in der Datenbank: " + rs.getInt(1)); }}using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"Fehler: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"Kunden in der Datenbank: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT name FROM customers WHERE email = '{email}'"; using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}def find(conn, email): sql = f"SELECT name FROM customers WHERE email = '{email}'" names = [name for (name,) in conn.execute(sql)] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("Fehler:", e)print("Kunden in der Datenbank:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()function find(db, email) { const sql = `SELECT name FROM customers WHERE email = '${email}'`; const names = db.prepare(sql).all().map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("Fehler:", e.message);}console.log("Kunden in der Datenbank:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); find(conn, "maria.ivanova@example.com"); find(conn, "x' OR '1'='1"); try { find(conn, "x'; DELETE FROM customers; --"); } catch (SQLException e) { System.out.println("Fehler: " + e.getMessage()); } try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) { rs.next(); System.out.println("Kunden in der Datenbank: " + rs.getInt(1)); }}maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"Fehler: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"Kunden in der Datenbank: {count.ExecuteScalar()}");maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 0conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("Fehler:", e)print("Kunden in der Datenbank:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]Fehler: You can only execute one statement at a time.Kunden in der Datenbank: 5const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("Fehler:", e.message);}console.log("Kunden in der Datenbank:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5Mit OR '1'='1' zeigen alle vier jeden Kunden. Eine zweite Anweisung im selben Text ist etwas anderes. In C# führt Microsoft.Data.Sqlite sie aus, und die Kunden sinken auf 0. In Java führt executeQuery und in JavaScript prepare nur die erste Anweisung aus und sagt zum Rest nichts, während Python mit einem Fehler abbricht. Der Treiber schützt dich nicht: Er bestimmt nur, wie groß der Schaden ist.
Die Idee
Der Parameter
Die Abfrage hat ein ? (in C# $email) an der Stelle, wo der Wert hingehört, und der Wert wird getrennt geschickt. Die Datenbank versteht zuerst die Abfrage, setzt dann den Wert ein, und der Wert bleibt Text, egal was darin steht:
static void find(Connection conn, String email) throws SQLException { String sql = "SELECT name FROM customers WHERE email = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, email); try (ResultSet rs = ps.executeQuery()) { List<String> names = new ArrayList<>(); while (rs.next()) { names.add(rs.getString("name")); } System.out.println(email + " → " + names); } }}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); find(conn, "maria.ivanova@example.com"); find(conn, "x' OR '1'='1"); try { find(conn, "x'; DELETE FROM customers; --"); } catch (SQLException e) { System.out.println("Fehler: " + e.getMessage()); } try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) { rs.next(); System.out.println("Kunden in der Datenbank: " + rs.getInt(1)); }}maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"Fehler: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"Kunden in der Datenbank: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM customers WHERE email = $email"; cmd.Parameters.AddWithValue("$email", email); using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5def find(conn, email): sql = "SELECT name FROM customers WHERE email = ?" names = [name for (name,) in conn.execute(sql, (email,))] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("Fehler:", e)print("Kunden in der Datenbank:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5function find(db, email) { const sql = "SELECT name FROM customers WHERE email = ?"; const names = db.prepare(sql).all(email).map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("Fehler:", e.message);}console.log("Kunden in der Datenbank:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []Kunden in der Datenbank: 5Jetzt ist x' OR '1'='1 nur eine E-Mail-Adresse, die es nicht gibt, und das Ergebnis ist leer. Wähle im Feld oben „Als Parameter“ und führe dieselben Eingaben aus: Die Abfrage, die bei der Datenbank ankommt, ändert sich nicht mehr.
Genauer, für Neugierige
Jede Sprache und jeder Treiber hat Parameter: PreparedStatement in Java, Parameters in C#, das zweite Argument von execute in Python, die Argumente von all / get / run in node:sqlite. PostgreSQL-Treiber benutzen $1, $2 (pg) oder %s (psycopg). Ein Parameter beschleunigt wiederholte Abfragen meist auch, weil die Datenbank eine schon verstandene Abfrage wiederverwenden kann.
Ein Parameter ist für Werte da. Tabellen- und Spaltennamen (zum Beispiel die Spalte in ORDER BY, aus einer Auswahlliste gewählt) können keine Parameter sein: Dort vergleicht das Programm die Eingabe mit einer Liste der erlaubten (price, name) und lässt nichts anderes durch.
Übung
Suche nach einem Teil der E-Mail-Adresse
Die Suche akzeptiert auch einen Teil einer E-Mail-Adresse. Das Programm schickt die Abfrage im Feld mit dem Parameter „petrov“. Ersetze ? durch den Wert, um zu sehen, was die Datenbank zurückgibt.
Lösung
SELECT name, email FROM customers WHERE email LIKE '%' || 'petrov' || '%';Das % davor und danach wird in der Abfrage selbst an den Wert gehängt ('%' || ? || '%'), nicht in der Eingabe. So bleibt der Parameter einfacher Text, und LIKE bekommt %petrov%.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: IN, BETWEEN und LIKE · Textfunktionen · DELETE: Zeilen löschen · Aus dem Code mit der Datenbank verbinden
- Geht weiter in: ORM und das N+1-Problem · Verbindungspools, Backups und Berechtigungen
- Aufgaben zum Thema: Die Datenbank im Programm