flawopen.com/SQL-Injection/Python
Stell dir ein Formular vor, das nur eine Ticketnummer wie 482 erwartet. SQL-Injection passiert, wenn jemand statt einer Zahl etwas Hinterhältiges in dieses Feld eingibt — eine Trickformulierung, die das System dazu bringt, "zeig mir alle Tickets" statt nur Ticket 482 zu sagen, weil das System nie geprüft hat, ob wirklich nur eine Zahl ankam.
SQL-Injection passiert, wenn benutzergesteuerte Eingabe direkt in den Text einer Datenbankabfrage eingefügt wird, statt als separater Wert übergeben zu werden. Wenn der Code eine Abfrage durch das Zusammenkleben von Zeichenketten baut, kann ein Angreifer eine Eingabe liefern, die die tatsächliche Struktur der Abfrage verändert — aus einer "eine Zeile abrufen"-Abfrage wird eine, die alle Zeilen zurückgibt, oder eine Tabelle löscht.
In Python zeigt sich das fast immer auf dieselbe Weise: ein Datenbankaufruf, der mit einem f-String, %-Formatierung oder einfacher +-Verkettung gebaut wird, statt der eingebauten Parameter-Platzhalter des Datenbanktreibers.
2015 erlitt der britische Telekommunikationsanbieter TalkTalk einen Datenschutzvorfall, der über 150.000 Kunden betraf, nachdem Angreifer eine SQL-Injection-Schwachstelle in einer alten Webseite ausnutzten, die aus einer Unternehmensübernahme stammte. Die britische Datenschutzaufsichtsbehörde verhängte gegen TalkTalk eine Geldstrafe von 400.000 £ und bezeichnete den Fehler als vermeidbar und grundlegend.
Quelle: Bußgeldbescheid des britischen Information Commissioner's Office, 2016 — siehe Referenzen unten.# user_id kommt direkt aus der Anfrage
def get_user(cursor, user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
return cursor.fetchone()
# Wert wird separat übergeben, nie eingebettet
def get_user(cursor, user_id):
query = "SELECT * FROM users WHERE id = %s"
cursor.execute(query, (user_id,))
return cursor.fetchone()
Die korrigierte Version übergibt den Abfragetext und den Wert als zwei getrennte Argumente an execute(). Der Datenbanktreiber sendet sie ebenfalls getrennt an die Datenbank — die Struktur der Abfrage steht fest, bevor der Wert daran angehängt wird, sodass der Wert niemals als Teil der SQL-Syntax interpretiert werden kann, unabhängig davon, welche Zeichen er enthält. Ein f-String kann das nicht leisten, denn wenn execute() die Abfrage erhält, ist der Wert bereits so in den Text eingebacken, als wäre er immer Teil des Befehls gewesen.
cursor.execute("... WHERE id = %s" % user_id) ist genauso verwundbar wie ein f-String. Der Platzhalter wird erst sicher, wenn der Wert als eigenes zweites Argument von execute() übergeben wird — cursor.execute("...WHERE id = %s", (user_id,)) —, sodass der Treiber und nicht Pythons String-Formatierung die Ersetzung vornimmt.
Das ORM von Django und der Query Builder von SQLAlchemy parametrisieren normale Abfragen automatisch. Das Risiko kehrt zurück, sobald Model.objects.raw() oder SQLAlchemys text() verwendet wird und dieses rohe SQL mit einem f-String gebaut wird.
psycopg2 (PostgreSQL) verwendet %s unabhängig vom Spaltentyp; sqlite3 verwendet ?. Einen Platzhalterstil aus der Dokumentation des einen Treibers in den anderen zu übernehmen, schlägt stillschweigend fehl — prüfe den Parameterstil deines konkreten Treibers, statt ihn anzunehmen.
Richtig für die normale Abfrage-API des ORM — falsch, sobald raw() oder text() verwendet und dieser String selbst gebaut wird.
Das Risiko liegt nicht im Typ des Werts zur Laufzeit — es liegt darin, dass die Abfrage überhaupt durch String-Interpolation gebaut wird. Sobald diese Annahme irgendwann im Lebenszyklus des Codes nicht mehr stimmt, ist die Schwachstelle bereits da und wartet.
Manuelles Escaping ist treiberspezifisch und leicht subtil falsch zu machen. Parametrisierte Abfragen sind keine strengere Form des Escapings — sie vermeiden das Problem vollständig, weil der Wert nie Teil des Abfragetexts wird.
grep -rn "execute(f\"" --include="*.py" .
grep -rn "execute(.*%\s*(" --include="*.py" .
grep -rn "\.raw(\|text(" --include="*.py" .
Bei seinen normalen Abfragemethoden ja. Seine Notausgänge für rohe Abfragen nicht — die sind genau so sicher wie handgeschriebenes SQL, nicht sicherer.
Nein. Alles, was faktisch von einer außenstehenden Partei kontrolliert wird, zählt — HTTP-Header, hochgeladene Dateinamen, sogar ein Wert aus einer Drittanbieter-API, der die Anwendung vertraut.
Das kannst du, aber es ist fragil und treiberspezifisch. Parametrisierte Abfragen sind die eigentliche Korrektur, keine strengere Form des Escapings.