Exkurs - Gefahren bei Benutzereingaben
Das folgende Beispiel solltest du auf einer neuen Datenbank ausprobieren; die (vereinfachten) Strukturen und Beispiel-Datensätze kannst du hier herunterladen.
Überraschendes Ergebnis einer Benutzereingabe
Mit folgendem Programmfragment (ganzes Programm hier zum Download) kann ein Fahrschüler wieder gelöscht werden. Probiere das Programm aus und lösche z.B. den Schüler "Andi Macht".
print("Löschen eines Fahrschülers")
print("==========================")
delName = input("Name : ")
SQLBefehl = "DELETE FROM schueler \
WHERE Name = '%s'" % (delName)
cursor = con.cursor()
try:
cursor.execute(SQLBefehl)
print("%i Datensätze gelöscht" % (cursor.rowcount))
except mysql.connector.Error as err:
print("Fehler bei der SQL-Ausführung: %s" % (err))
raise # Fehler - also Stopp der Verarbeitung
cursor.close()
con.commit()
Abgesehen davon, dass es bestimmt keine gute Idee ist, nur den Nachnamen abzufragen
(es gibt sicherlich mehrere "Meier" in einer realen Datenbank), stellt sich hier noch ein weiteres Problem:
Betrachte den folgenden Dialog mit dem Programm. Wie kommt die hoffentlich überraschende Ausgabe zustande?
Im Beispiel wurde die Einschränkung der Datensätze durch das Erweitern um OR '1'='1'
ausgehebelt. Damit werden alle Datensätze gelöscht. Und noch viel schlimmer: In der Datenbank war beim
Fremdschlüssel in der fahrstunde-Tabelle die Anweisung ON DELETE CASCADE
hinterlegt. Damit
wurden auch dort alle Datensätze gelöscht, da jetzt die Schlüsselpartner fehlen.
Das Ausprobieren von SQL-Injection auf fremden Seiten ist strafbar nach § 202a des Strafgesetzbuches (Ausspähen von Daten):
(1) Wer unbefugt sich oder einem anderen Zugang zu Daten, die nicht für ihn bestimmt und die gegen unberechtigten Zugang besonders gesichert sind, unter Überwindung der Zugangssicherung verschafft, wird mit Freiheitsstrafe bis zu drei Jahren oder mit Geldstrafe bestraft.
... es geht noch schlimmer
Dieses Beispiel funktioniert in dieser Form nur mit MySQL.
Aber auch für SQLite lassen sich ähnliche Probleme erzeugen.
Nehmen wir an, es soll im Programm in einer Logtabelle mitgeschrieben werden, wenn z.B. Daten eines Schülers ausgelesen werden. Der Programmierer hat das so gelöst, dass beim SELECT-Befehl zuvor noch ein INSERT in die Logtabelle erzeugt wird. Zur Vereinfachung behandelt er diese beiden Befehle in einem Multi-Cursor, der mehrere SQL-Befehle abarbeitet.
Du kannst das selbst einmal ausprobieren. (Beispielprogramm):
cursor = con.cursor()
selName = input("Gesuchter Schüler: ")
SQLBefehle = '''
CREATE TABLE IF NOT EXISTS log (Zugriff DATETIME);
INSERT INTO log VALUES (NOW());
SELECT SNR, Name, Vorname
FROM schueler
WHERE Name LIKE '%s'
''' % (selName)
# wir gehen mal auf Nummer sicher...
try:
for result in cursor.execute(SQLBefehle, multi=True):
if result.with_rows:
row = result.fetchone()
while row!=None:
print(row[0],row[1],row[2])
row = result.fetchone()
except mysql.connector.Error as err:
print("Fehler bei der SQL-Ausführung: %s" % (err))
raise # Fehler - also Stopp der Verarbeitung
cursor.close()
con.commit()
Der folgende Dialog zeigt aber ein gravierendes Problem auf:
Hier werden gleich zwei weitere SQL-Befehle "eingeschleust". Da es sich um DDL-Kommandos handelt, werden diese auch direkt ausgeführt - auch wenn noch nachfolgend eine Fehlermeldung erzeugt wird (falsches Ende der Anweisung) und somit das commit fehlt.
Aufgabe 1
Wie müsste die SQL-Injection angepasst werden, so dass die Fehlermeldung vermieden wird?
Aufgabe 2
Schreibe eine SQL-Injection, die einen weiteren Schüler einfügt.
Abhilfen
Um SQL-Injection zu vermeiden gibt es verschiedene Maßnahmen bei der Programmierung, u.a.:
- Beschränken der Nutzerrechte: Wenn nur SELECT-Abfragen gewünscht sind, dann sollte auch nur das SELECT-Recht eingeräumt werden.
- Abfrage der Ergebnisse bei DML-Befehlen: Wenn ein DELETE nur einen Datensatz löschen soll und
cursor.rowcount
mehr als einen Datensatz liefert, dann ist etwas falsch gelaufen. In diesem Fall können mitcon.rollback()
stattcon.commit()
die Änderungen rückgängig gemacht werden. - Multi-Cursor sollten grundsätzlich vermieden werden: Im Beispiel oben hätten es auch einzelne Cursor getan, die jeweils auch nur einen SQL-Befehl verarbeiten können.