0%

Eingaben, die zu Code werden

Daten, die als Anweisungen gelesen werden

Hier ist ein Titel: <b>Dune</b>. Nach den Regeln, die du in Lektion 2 geschrieben hast, ist er ein gültiger Titel. Er ist Text, er ist 11 Zeichen lang, und nichts im Vertrag verbietet eine spitze Klammer. Führe diesen Block aus und sieh dir an, was an zwei Stellen mit ihm passiert:

Try it

Die zweite Ausgabezeile ist das Buch als : {"title": "<b>Dune</b>", "author": "Frank Herbert"}. Der Titel steht zwischen zwei Anführungszeichen, und wer dieses JSON parst, bekommt dieselben 11 Zeichen zurück. Er ist Daten.

Die dritte Zeile ist <li><b>Dune</b></li>. Nichts markiert mehr, wo der Titel anfängt oder aufhört. Ein Browser, der diese Zeile liest, findet ein <b>-Tag in einem Listeneintrag. Die 11 Zeichen haben sich nicht geändert. Geändert hat sich, wer sie liest, und dieser Leser nimmt einen Teil davon als Anweisungen.

Das Muster

Jede Schwachstelle im Rest dieses Kapitels hat dieselben drei Schritte.

  1. Text von außen wird in einen größeren Text eingefügt.

  2. Ein Parser liest das Ergebnis: ein Browser, ein -Client, ein , eine Datenbank.

  3. Der Parser kann nicht erkennen, welcher Teil die Daten waren. Er sieht einen Text und liest ihn ganz nach seinen eigenen Regeln.

Das nennt man Injection: Daten, die in einen Text eingefügt wurden, werden als Teil der Anweisungen dieses Texts gelesen.

Kapitel 1 hat die Vertrauensgrenze dorthin gelegt, wo eine Anfrage hereinkommt. Injection passiert dort, wo eine Grenze ein zweites Mal überquert wird, auf dem Weg hinaus: wo dein Server Text an etwas anderes übergibt, das ihn parsen wird.

Vier Stellen

Hier sind vier Fragmente. Jedes hat eine Zeile, in der Daten in Text übergehen, der geparst wird.

Eine Datenbankabfrage. Die Datenbank parst das Ergebnis als SQL:

# title is joined into SQL
query = f"SELECT author FROM books WHERE title = '{title}'"

Ein . Der Client parst den Kopf der Antwort Zeile für Zeile:

# target is joined into a header
line = "Location: " + target + "\r\n"

Ein Dateipfad. Das Betriebssystem liest / und .. darin als Wegweiser:

# name is joined into a path
cover = pathlib.Path(folder) / name

Eine Seite. Der Browser parst sie als HTML:

# title is joined into markup
row = "<li>" + title + "</li>"

Deine Leseliste hat die letzten drei, seit dieses Kapitel begonnen hat. Um die zweite in Aktion zu sehen, baut der nächste Block den Kopf einer als einfachen Text, so wie Kapitel 2 von HTTP with Python Anfragen von Hand geschrieben hat. Der Titel darin enthält einen Zeilenumbruch:

Try it

Ein Client teilt den Kopf an jedem \r\n, wie die es tut, und die Klammern zeigen, wo jede Zeile anfängt und aufhört. Der Server hat zwei Zeilen geschrieben, eine Statuszeile und eine Location-Zeile. Der Client liest drei, und die dritte, [Set-Cookie: note=x], kam aus dem Titel. Die leeren Klammerpaare am Ende sind die Leerzeile, die den Kopf beendet, und der leere Text danach.

Ein Server baut einen Header mit line = "Location: " + target + "\r\n". Wo werden Daten zu Anweisungen?

Dieselbe Schleife bekommt einen Titel mit einer Leerzeile darin. Welche Zeile gibt sie als dritte aus?

title = "Dune\r\n\r\n<p>hi</p>"

head = "HTTP/1.1 303 See Other\r\nLocation: /books?title=" + title + "\r\n\r\n"

for line in head.split("\r\n"):
    print("[" + line + "]")

Ein erster Blick auf SQL

Das erste Fragment ist das bekannteste der vier. Es heißt SQL-Injection, und das ist schon alles:

query = "SELECT author FROM books WHERE title = '" + title + "'"

Ein Titel mit einem einfachen Anführungszeichen (') darin beendet den Text in Anführungszeichen zu früh, und alles nach dem Anführungszeichen wird als SQL gelesen. Die Lösung ist, mit dem Einfügen aufzuhören. Die Abfrage behält einen Platzhalter, das ?, und der reist daneben mit:

cursor.execute("SELECT author FROM books WHERE title = ?", (title,))

Jetzt bekommt die Datenbank die Abfrage und den Wert als zwei getrennte Dinge, und sie parst den Wert nie als SQL. Nichts in diesem Kurs führt SQL aus, also ist das ein erster Blick und nicht mehr. Der Kurs SQL and Databases with Python bringt dir Platzhalter richtig bei.

Zwei weitere Stellen haben dasselbe Muster, und dieser Kurs nennt sie nur. Text von außen, der in einen Shell-Befehl eingefügt wird, wird von der Shell geparst. Und Pythons pickle kann Code ausführen, während es gepickelte Bytes lädt, also werden Bytes von außen nie entpickelt.

Validierung ist nicht die Lösung

Der Titel war gültig. Das ist der unbequeme Teil.

Es ist verlockend, zu validate_book zurückzugehen und < in Titeln abzulehnen. Das macht ehrliche Titel kaputt, die das Zeichen enthalten, und verfehlt trotzdem die anderen Stellen. Der Header wurde mit einem Zeilenumbruch angegriffen, nicht mit einer spitzen Klammer. Der Pfad wird mit .. angegriffen werden. Ein Validator müsste jede Stelle kennen, an die ein Wert jemals geschrieben werden könnte, und jedes Zeichen, das dort etwas bedeutet.

validate_book hat den Titel <b>Dune</b> angenommen. Warum ist er in der Seite trotzdem ein Problem?

Die Verteidigung gehört also an jede Stelle, an der der Text verwendet wird, in den Code, der weiß, was er schreibt. An jeder Stelle ist es eine andere Verteidigung, und die nächsten vier Lektionen schreiben je eine:

Wohin der Text gehtDie Verteidigung
Ein Header-WertEinen Wert mit Zeilenumbruch ablehnen
Ein WeiterleitungszielNur eine Form erlauben: einen Pfad auf dieser Site
Ein DateipfadDen Pfad auflösen und dann mit dem Ordner vergleichen
Eine SeiteDie Zeichen kodieren, die in HTML etwas bedeuten

Wohin gehört die Verteidigung gegen einen Titel, der zu Markup wird?

Validierung bleibt wichtig: Sie entscheidet, was gespeichert werden darf. Sie ersetzt nicht die Arbeit am anderen Ende. Validiere auf dem Weg hinein, schütze oder kodiere auf dem Weg hinaus, und lass nie das eine für das andere einstehen.

Die Blöcke auf dieser Seite bauen nur Text. Hier wird kein Header gesendet und keine Datei geöffnet. In der nächsten Lektion schreibt dein eigener Server den Header, und eine Anfrage entscheidet, was darin steht.