Zum Inhalt springen
aviral gupta

// A5.3 · ca. 35 Min. · Vertiefung

sqlite3 und parametrisierte Abfragen

Nach dieser Lektion fragen Sie SQLite aus Python mit ? und benannten Platzhaltern ab, fügen viele Zeilen mit executemany ein, machen Änderungen per Transaktion ganz oder gar nicht und erklären, warum aus Strings gebautes SQL zur SQL-Injection einlädt.

Lektion 3 von 5 in A5 Modernes Python und sicherer Code

Danach können Sie

  • Werte mit ? und benannten Platzhaltern an SQL übergeben, nie durch Zusammenbauen des SQL-Strings
  • Viele Zeilen mit executemany einfügen und Transaktionen mit commit, rollback und with con steuern
  • Erklären, wie SQL-Injection funktioniert und warum Platzhalter sie verhindern
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der vorigen Lektion: Was gibt das aus?

    import tomllib
    
    limit = tomllib.loads("limit = 10")["limit"]
    print(limit + 1, type(limit).__name__)
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. So heißt niemand, was gibt das also aus?

    import sqlite3
    
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE users(name, secret)")
    con.executemany("INSERT INTO users VALUES (?, ?)", [("ada", "a1"), ("bob", "b2")])
    
    name = "nobody' OR '1'='1"  # typed into a search box
    sql = f"SELECT name FROM users WHERE name = '{name}'"  # the mistake
    print(con.execute(sql).fetchall())
  3. Üben · Aufgabe 3 von 7

    Setzen Sie das Parameter-Argument ein, das name an den Platzhalter ? bindet.

    import sqlite3
    
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE users(name, secret)")
    con.executemany("INSERT INTO users VALUES (?, ?)", [("ada", "a1"), ("bob", "b2")])
    
    name = "ada"
    rows = con.execute("SELECT secret FROM users WHERE name = ?", ____)
    print(rows.fetchall())
    WHERE name = ?", )
  4. Üben · Aufgabe 4 von 7

    Benannte Platzhalter nehmen ein dict. Was gibt das aus?

    import sqlite3
    
    con = sqlite3.connect(":memory:")
    row = con.execute("SELECT :a + :b", {"a": 2, "b": 3}).fetchone()
    print(row)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Teil der sqlite3-API zu, was er tut.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das zweite INSERT verletzt die UNIQUE-Regel, und der Fehler wird abgefangen. Wie viele Zeilen bleiben?

    import sqlite3
    
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT UNIQUE)")
    try:
        with con:
            con.execute("INSERT INTO t(name) VALUES (?)", ("a",))
            con.execute("INSERT INTO t(name) VALUES (?)", ("a",))
    except sqlite3.IntegrityError:
        pass
    print(con.execute("SELECT count(*) FROM t").fetchone()[0])
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie für eine Tabelle score(player, points) add_scores(con, rows), wobei rows eine Liste von dicts mit den Schlüsseln player und points ist, und top(con, minimum), das die Spieler mit mindestens minimum Punkten liefert, die besten zuerst. Nutzen Sie benannte Platzhalter beim Einfügen, ? bei der Abfrage und with con:. Probieren Sie dann top(con, "50 OR 1=1").

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Eine kleine Bücherdatenbank

Das Programm legt eine Tabelle im Speicher an und fügt mit executemany in with con: drei Bücher ein. by_author() übergibt den Autor als Parameter, ein Injection-Versuch ist also nur ein Name, der nichts findet. Ein benannter Platzhalter füllt :year. Zum Schluss scheitert eine Transaktion mit einem gültigen und einem doppelten Buch, und with con: rollt beide Einfügungen zurück: Die Tabelle enthält weiter drei Bücher.

main.py

import sqlite3

con = sqlite3.connect(":memory:")  # a database that lives in memory only
con.execute("CREATE TABLE book(title TEXT UNIQUE, author TEXT, year INTEGER)")

books = [
    ("Dune", "Frank Herbert", 1965),
    ("Emma", "Jane Austen", 1815),
    ("Persuasion", "Jane Austen", 1817),
]
with con:  # commits when the block succeeds
    con.executemany("INSERT INTO book VALUES (?, ?, ?)", books)


def by_author(author: str) -> list[str]:
    rows = con.execute("SELECT title FROM book WHERE author = ? ORDER BY year", (author,))
    return [title for (title,) in rows]


print(by_author("Jane Austen"))
print(by_author("x' OR '1'='1"))  # just text: no author has this name

query = "SELECT count(*) FROM book WHERE year < :year"
print(con.execute(query, {"year": 1900}).fetchone())

try:
    with con:  # rolls back when the block raises
        con.execute("INSERT INTO book VALUES (?, ?, ?)", ("Ulysses", "James Joyce", 1922))
        con.execute("INSERT INTO book VALUES (?, ?, ?)", ("Emma", "Someone Else", 2000))
except sqlite3.IntegrityError as err:
    print("rolled back:", err)

print(con.execute("SELECT count(*) FROM book").fetchone()[0], "books")
con.close()

Ausführen mit

python main.py

Ausgabe

['Emma', 'Persuasion']
[]
(2,)
rolled back: UNIQUE constraint failed: book.title
3 books
  • for (title,) in rows entpackt jedes einspaltige Zeilentupel.
  • Der Injection-Versuch liefert [], denn der ganze String wird mit author verglichen.
  • Ulysses wurde eingefügt und mit der doppelten Emma zurückgerollt: weiter 3 Bücher.
  • with con: schließt die Verbindung nie, deshalb steht am Ende con.close().
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Zwei angreifbare Abfragen reparieren

Beide Funktionen in main.py bauen SQL mit f-Strings. Ein Autor wie O'Brien bringt sie zum Absturz, und eine präparierte Eingabe liefert alle Titel. Schreiben Sie sie mit ?-Platzhaltern neu. search_titles(con, word) muss weiterhin Titel finden, die word irgendwo enthalten, die %-Platzhalterzeichen gehören also jetzt in den gebundenen Wert.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Das SQL wird konstant: "SELECT title FROM book WHERE author = ? ORDER BY title", mit (author,) als zweitem Argument.

  2. Hinweis 2

    Ein ? darf nicht in Anführungszeichen stehen. Schreiben Sie title LIKE ? und binden Sie f"%{word}%".

  3. Hinweis 3

    Denken Sie an das Komma: (author,) ist ein Tupel, (author) nur ein String.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import sqlite3


def find_by_author(con: sqlite3.Connection, author: str) -> list[str]:
    """Titles by exactly this author, in title order."""
    sql = "SELECT title FROM book WHERE author = ? ORDER BY title"
    return [row[0] for row in con.execute(sql, (author,))]


def search_titles(con: sqlite3.Connection, word: str) -> list[str]:
    """Titles that contain word anywhere, in title order."""
    sql = "SELECT title FROM book WHERE title LIKE ? ORDER BY title"
    return [row[0] for row in con.execute(sql, (f"%{word}%",))]
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import sqlite3


def find_by_author(con: sqlite3.Connection, author: str) -> list[str]:
    """Titles by exactly this author, in title order."""
    sql = f"SELECT title FROM book WHERE author = '{author}' ORDER BY title"
    return [row[0] for row in con.execute(sql)]


def search_titles(con: sqlite3.Connection, word: str) -> list[str]:
    """Titles that contain word anywhere, in title order."""
    sql = f"SELECT title FROM book WHERE title LIKE '%{word}%' ORDER BY title"
    return [row[0] for row in con.execute(sql)]

test_main.py

import sqlite3

from main import find_by_author, search_titles


def make_db():
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE book(title TEXT, author TEXT)")
    con.executemany(
        "INSERT INTO book VALUES (?, ?)",
        [
            ("Emma", "Jane Austen"),
            ("Persuasion", "Jane Austen"),
            ("Dune", "Frank Herbert"),
            ("At Swim-Two-Birds", "Flann O'Brien"),
        ],
    )
    return con


def test_author():
    """find_by_author findet beide Titel von Austen"""
    got = find_by_author(make_db(), "Jane Austen")
    assert got == ["Emma", "Persuasion"], f"find_by_author lieferte {got!r}"


def test_apostrophe():
    """Ein Name mit Apostroph funktioniert"""
    got = find_by_author(make_db(), "Flann O'Brien")
    assert got == ["At Swim-Two-Birds"], f"find_by_author lieferte {got!r}"


def test_author_injection():
    """Ein Injection-Versuch ist nur ein Name, der nichts findet"""
    got = find_by_author(make_db(), "x' OR '1'='1")
    assert got == [], f"find_by_author lieferte {got!r}: binden Sie den Wert mit ?"


def test_search():
    """search_titles findet Titel, die das Wort enthalten"""
    got = search_titles(make_db(), "u")
    assert got == ["Dune", "Persuasion"], f"search_titles lieferte {got!r}"


def test_search_injection():
    """Anführungszeichen im Suchwort können die Abfrage nicht ändern"""
    got = search_titles(make_db(), "' OR 1=1 OR title = '")
    assert got == [], f"search_titles lieferte {got!r}: die %-Zeichen gehören in den gebundenen Wert"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Eine Überweisung, ganz oder gar nicht

transfer(con, src, dst, amount) verschiebt Geld zwischen Zeilen von account(name, balance). Machen Sie es sicher: Führen Sie beide Updates in with con: aus und lösen Sie ValueError aus, wenn src oder dst nicht existiert (prüfen Sie cur.rowcount) oder src unter null fallen würde. Jeder ValueError im Block muss alle Änderungen zurückrollen, und eine gelungene Überweisung muss committet sein.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Lösen Sie innerhalb des with con:-Blocks aus: Der Rollback geschieht, weil der Block mit einer Ausnahme endet.

  2. Hinweis 2

    cur.rowcount ist die Zahl der Zeilen, die das UPDATE geändert hat; 0 heißt, es gibt kein solches Konto.

  3. Hinweis 3

    Lesen Sie nach dem ersten UPDATE den neuen Kontostand mit einem SELECT und lösen Sie ValueError aus, wenn er unter 0 liegt.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import sqlite3


def transfer(con: sqlite3.Connection, src: str, dst: str, amount: int) -> None:
    """Move amount from src to dst: both updates happen, or neither does."""
    if amount <= 0:
        raise ValueError("amount must be positive")
    with con:  # commit on success, roll back if anything below raises
        cur = con.execute("UPDATE account SET balance = balance - ? WHERE name = ?", (amount, src))
        if cur.rowcount != 1:
            raise ValueError(f"no account {src!r}")
        (left,) = con.execute("SELECT balance FROM account WHERE name = ?", (src,)).fetchone()
        if left < 0:
            raise ValueError(f"{src!r} has too little money")
        cur = con.execute("UPDATE account SET balance = balance + ? WHERE name = ?", (amount, dst))
        if cur.rowcount != 1:
            raise ValueError(f"no account {dst!r}")
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import sqlite3


def transfer(con: sqlite3.Connection, src: str, dst: str, amount: int) -> None:
    """Move amount from src to dst: both updates happen, or neither does."""
    # Nothing is committed, and nothing stops an overdraft or an unknown account.
    con.execute("UPDATE account SET balance = balance - ? WHERE name = ?", (amount, src))
    con.execute("UPDATE account SET balance = balance + ? WHERE name = ?", (amount, dst))

test_main.py

import sqlite3

from main import transfer


def make_db():
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE account(name TEXT PRIMARY KEY, balance INTEGER)")
    con.executemany("INSERT INTO account VALUES (?, ?)", [("ada", 100), ("bob", 50)])
    con.commit()
    return con


def balances(con):
    return dict(con.execute("SELECT name, balance FROM account"))


def raises_value_error(con, *args):
    try:
        transfer(con, *args)
    except ValueError:
        return True
    return False


def test_moves_money():
    """30 gehen von ada an bob"""
    con = make_db()
    transfer(con, "ada", "bob", 30)
    got = balances(con)
    assert got == {"ada": 70, "bob": 80}, f"die Kontostände sind {got!r}"


def test_committed():
    """Die Überweisung wird committet"""
    con = make_db()
    transfer(con, "ada", "bob", 30)
    assert not con.in_transaction, "eine Transaktion ist noch offen: mit with con: wird sie committet"


def test_overdraft():
    """Zu wenig Geld löst ValueError aus und ändert nichts"""
    con = make_db()
    raised = raises_value_error(con, "ada", "bob", 500)
    got = balances(con)
    assert raised and got == {"ada": 100, "bob": 50}, f"ValueError ausgelöst: {raised}, Kontostände {got!r}"


def test_unknown_account():
    """Ein unbekannter Empfänger löst ValueError aus, und ada behält ihr Geld"""
    con = make_db()
    raised = raises_value_error(con, "ada", "eve", 10)
    got = balances(con)
    assert raised and got == {"ada": 100, "bob": 50}, f"ValueError ausgelöst: {raised}, Kontostände {got!r}: die erste Änderung zurückrollen"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Den Wert selbst in Anführungszeichen setzen

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name)")
name = "O'Brien"
con.execute(f"SELECT * FROM users WHERE name = '{name}'")

Was Python ausgibt

sqlite3.OperationalError: near "Brien": syntax error

Warum, und die Lösung

Der Apostroph in O'Brien beendet den SQL-String zu früh, und SQLite liest Brien dann als SQL. Anführungszeichen von Hand zu verdoppeln ist fehleranfällig. Nehmen Sie einen Platzhalter: con.execute("SELECT * FROM users WHERE name = ?", (name,)). Der Wert wird nie Teil des SQL-Textes.

Ein String, wo ein Tupel hingehört

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name)")
con.execute("SELECT * FROM users WHERE name = ?", "ada")

Was Python ausgibt

sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 3 supplied.

Warum, und die Lösung

parameters muss eine Sequenz von Werten sein, und ein str ist eine Sequenz von Zeichen: "ada" liefert drei Werte. Übergeben Sie ein Tupel, ("ada",), mit Komma, oder eine Liste, ["ada"].

Benannte Platzhalter mit einer Liste

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("SELECT :name", ["ada"])

Was Python ausgibt

sqlite3.ProgrammingError: Binding 1 (':name') is a named parameter, but you supplied a sequence which requires nameless (qmark) placeholders.

Warum, und die Lösung

Ein benannter Platzhalter wird über seinen Namen gefunden, er braucht also ein dict: con.execute("SELECT :name", {"name": "ada"}). Listen und Tupel passen nur zu ?-Platzhaltern.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Platzhalter tragen die Werte

sqlite3.connect(":memory:") öffnet eine Datenbank im Speicher; ein Dateiname öffnet oder erzeugt eine Datei. con.execute(sql, parameters) führt eine Anweisung aus. Schreiben Sie ein ? dorthin, wo ein Wert hingehört, und übergeben Sie die Werte als Sequenz: con.execute("... WHERE name = ?", (name,)); beachten Sie das Komma des einelementigen Tupels. Benannte Platzhalter, :name, nehmen ein dict, überzählige Schlüssel werden ignoriert. Stimmt die Anzahl nicht, folgt ProgrammingError. Platzhalter stehen nur für Werte: Tabellen- und Spaltennamen lassen sich nicht binden, wählen Sie sie aus einer festen Liste im Code.

executemany und Transaktionen

con.executemany(sql, rows) führt ein INSERT, UPDATE oder DELETE je Element von rows aus. Standardmäßig öffnet eine solche Anweisung implizit eine Transaktion, und erst con.commit() speichert ihre Änderungen; con.rollback() verwirft sie, und Schließen ohne commit verliert sie. Am einfachsten ist with con:. Es committet, wenn der Block normal endet, und rollt zurück, wenn er eine Ausnahme auslöst, die dann weiterläuft. So werden mehrere Anweisungen zu ganz oder gar nicht, wie bei einer Überweisung nötig. with con: schließt die Verbindung nicht: Rufen Sie con.close() selbst auf.

SQL-Injection, gezeigt und behoben

Mit einem f-String gebaut, lässt f"... WHERE name = '{name}'" die Eingabe SQL schreiben: Wer x' OR '1'='1 eintippt, schließt das Anführungszeichen und hängt eine immer wahre Bedingung an, die Abfrage liefert also alle Zeilen. Dieselbe Bauweise bricht auch bei harmlosen Namen: O'Brien ist ein Syntaxfehler. Mit Platzhalter kompiliert SQLite erst die Anweisung und bekommt den Wert getrennt, ein Anführungszeichen darin ist nur ein Zeichen. Selbst maskieren müssen Sie nichts. Das gilt auch für LIKE: Die %-Zeichen gehören in den gebundenen Wert, f"%{word}%", nicht ins SQL.

Quellen

Zuletzt geprüft am 29. September 2026