flawopen.com/SQL-Injection/Python

SQL-Injection in Python

Kritisch CWE-89 Entwurf — wartet auf Prüfung
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
Einfach erklärt

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.

Schlüsselbegriffe auf dieser Seite
benutzergesteuerte Eingabe
Jeder Wert, der letztlich von der Person stammt, die die Anwendung nutzt — oder angreift: ein Formularfeld, ein URL-Parameter, ein hochgeladener Dateiname, ein HTTP-Header. Die Anwendung darf nicht annehmen, dass er wohlgeformt oder sicher ist.
SQL-Abfrage
Der an eine Datenbank gesendete Befehl — z. B. "diese Zeile holen", "diese Tabelle löschen". Seine Bedeutung ergibt sich vollständig aus seinem exakten Text, was das Einschleusen von zusätzlichem Text so gefährlich macht.

Was passiert

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.

Auswirkung in der Praxis

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.

Verwundbar vs. behoben

VERWUNDBAR
# 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()
BEHOBEN
# 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()

Warum die Korrektur funktioniert

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.

Python-spezifische Fallstricke

%-Formatierung innerhalb von execute() sieht noch parametrisiert aus — ist es aber nicht

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.

ORMs parametrisieren standardmäßig, ihre Notausgänge aber nicht

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.

Die Platzhaltersyntax ist zwischen Treibern nicht einheitlich

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.

Verbreitete Irrtümer

"Mein ORM schützt mich automatisch"

Richtig für die normale Abfrage-API des ORM — falsch, sobald raw() oder text() verwendet und dieser String selbst gebaut wird.

"Diese ID ist immer eine Zahl, also ist Interpolation sicher"

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.

"Ich escape Anführungszeichen selbst, brauche also keine parametrisierten Abfragen"

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.

So prüfst du, ob du betroffen bist

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
Besser als reines Grep: Bandit (Regel B608, hardcoded_sql_expressions) im CI laufen lassen — es erkennt dieses Muster automatisch und lässt den Build bei einem neuen Vorkommen fehlschlagen.

Präventions-Checkliste

Häufig gestellte Fragen

Schützt mich ein ORM vor SQL-Injection?

Bei seinen normalen Abfragemethoden ja. Seine Notausgänge für rohe Abfragen nicht — die sind genau so sicher wie handgeschriebenes SQL, nicht sicherer.

Betrifft das nur Dinge wie Suchfelder?

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.

Kann ich Anführungszeichen nicht einfach selbst escapen?

Das kannst du, aber es ist fragil und treiberspezifisch. Parametrisierte Abfragen sind die eigentliche Korrektur, keine strengere Form des Escapings.

Referenzen

Ansehen in: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (N/A)
Siehe auch: Command InjectionPath Traversal XSSUnsichere Deserialisierung