Zum Inhalt
wannacode

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.

Die Seite sucht einen Kunden anhand der E-Mail-Adresse.Datenbank: Shop

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:

SQLDatenbank: Shop

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));
}
}
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));
}
}
Ausgabe
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: 5

Mit 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));
}
}
Ausgabe
maria.ivanova@example.com → [Maria Ivanova]
x' OR '1'='1 → []
x'; DELETE FROM customers; -- → []
Kunden in der Datenbank: 5

Jetzt 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.

SQLDatenbank: Shop

Test

Teste dich

1. Das Programm baut die Abfrage so: "... WHERE email = '" + email + "'". Was macht sie sicher?
2. Warum liefert „x' OR '1'='1“ alle Kunden?
3. Kann ein Parameter den Spaltennamen in ORDER BY ersetzen (ORDER BY ?)?
4. In Python löst cursor.execute() mit zwei Anweisungen im Text einen Fehler aus. Heißt das, dass Injection in Python nicht gefährlich ist?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·