0%

Eingaben, die zu Code werden · Übung

Leite nur auf deine eigene Site weiter

Lektion 4 hat dafür gesorgt, dass next deiner Antwort keine Zeile hinzufügen kann. Sie hat nicht gefragt, wohin next zeigt. Führe das Szenario noch einmal aus:

python pl_sec.py injection

Sein zweiter Teil dreht sich um diese Frage:

--- a redirect: ana logs in, and next names another site ---
POST /login?next=%2F%2Fother.example%2Fwelcome  -> 303
  location: //other.example/welcome
a browser that follows this leaves your site for other.example

Das Ziel enthielt %2F%2Fother.example%2Fwelcome. parse_qs hat jedes %2F zu einem Schrägstrich dekodiert, also ist der //other.example/welcome. Das ist eine saubere Zeile, und header_value lässt sie durch.

with Python, Kapitel 6, hat gezeigt, was ein Client mit einer location macht. Er löst den Wert gegen die auf, die er gerade angefragt hat, und ein Wert, der mit // beginnt, ersetzt den Host. Nur das Schema bleibt erhalten, also geht der Client weiter zu https://other.example/welcome.

Sieh es jetzt von der Seite des Lesers. Jemand schickt ihm einen Link zu deinem Login, mit deinem Hostnamen darin. Er prüft den Namen und tippt sein Passwort ein. Das Login ist echt und gelingt, und die Seite, die danach kommt, gehört jemand anderem. Diese Seite kann aussehen wie deine und noch einmal nach dem Passwort fragen. Ein Server, der dorthin weiterleitet, wohin eine Anfrage es sagt, ist ein Open Redirect.

Wie ein lokaler Pfad aussieht

next gibt es, um den Leser zu einer anderen Seite der Leseliste zu schicken. Ein solcher Wert ist ein Pfad auf dieser Site. Er beginnt mit einem /, etwa /books oder /books?limit=5.

Vier Arten von Werten sind nicht lokal:

  • //other.example nennt einen Host, wie du gerade gesehen hast.

  • https://other.example/ ist eine ganze URL, mit Schema und Host.

  • /\other.example beginnt mit einem Schrägstrich, aber ein Browser behandelt einen Backslash in einer URL wie einen Schrägstrich. Er liest //other.example.

  • Ein Wert mit einem Tab oder einem Zeilenumbruch darin, etwa /, ein Tab und dann /other.example. Ein Browser entfernt Tabs und Zeilenumbrüche aus einer URL, bevor er sie liest, und was übrig bleibt, ist wieder //other.example.

Die letzten beiden sind wichtig, weil es darauf ankommt, wer der folgt. Der Wert muss für das Programm sicher sein, das ihn als Nächstes liest, und dieses Programm ist ein Browser.

Warum wird /\other.example abgelehnt, obwohl es mit einem Schrägstrich beginnt?

Frag den Parser, aber verlass dich nicht allein auf ihn

Du kennst urlsplit aus dem Router. Sein Ergebnis hat ein scheme und ein netloc, den Host-Teil, und beide sind bei einem einfachen Pfad leer. Frag es nach vier Werten. Im Python-Quelltext ist "/\\other.example" der Text /\other.example.

from urllib.parse import urlsplit

for value in ["/books?limit=5", "//other.example/x", "https://other.example/", "/\\other.example"]:
    parts = urlsplit(value)
    print(value, "| scheme: [" + parts.scheme + "] host: [" + parts.netloc + "]")

Es gibt aus:

/books?limit=5 | scheme: [] host: []
//other.example/x | scheme: [] host: [other.example]
https://other.example/ | scheme: [https] host: [other.example]
/\other.example | scheme: [] host: []

Die ersten drei sind, was du erwarten würdest. Der vierte ist das Problem: Python sieht in /\other.example keinen Host, und ein Browser schon. Zwei Parser lesen denselben Text und sind sich uneinig, und der Browser ist derjenige, der danach handelt.

Deshalb überlässt die die Entscheidung nicht urlsplit. Sie prüft zuerst selbst die Form des Texts und fragt urlsplit zuletzt, als zweite Meinung. Zuletzt ist noch aus einem weiteren Grund wichtig: urlsplit löst für einige Werte, die mit // beginnen, aus, etwa für //[, und die Schrägstrich-Regel hat diese schon beantwortet.

Was gibt das aus?

from urllib.parse import urlsplit

print("[" + urlsplit("//other.example/x").netloc + "]", "[" + urlsplit("/\\other.example").netloc + "]")

Erlaube eine Form

Lektion 1 hat die Schlüssel aufgelistet, die ein Buch haben darf, und den Rest abgelehnt. Das ist dieselbe Idee für Formen. Sammle keine gefährlichen Werte. Beschreibe die eine Form, die sicher ist, in vier Regeln:

  1. Nur druckbares ASCII: value.isascii() and value.isprintable(). isprintable() ist False für einen Tab, einen Zeilenumbruch und jedes andere Steuerzeichen. Die ASCII-Hälfte stoppt auch einen Buchstaben wie é, weil http_adapter Header als Latin-1 schreibt und ein solches Zeichen nicht so ankäme, wie es gesendet wurde.

  2. Nirgends ein Backslash.

  3. Ein Schrägstrich am Anfang, und nicht zwei.

  4. Kein Schema und kein Host, laut urlsplit.

safe_redirect_target(value) gibt den Wert zurück, wenn alle vier Regeln gelten. Für alles andere gibt es "/" zurück. Es löst nie etwas aus, und das Login gelingt trotzdem: Das Passwort war richtig, also ist ein seltsames next kein Grund, es abzulehnen. Der Leser landet auf der Startseite.

Ein next mit Zeilenumbruch ist nicht druckbar, also wird es von jetzt an ebenfalls zu /, und das Login antwortet mit 303. Die 400 aus Lektion 4 ist über das Login nicht mehr erreichbar. Wenn dein Test aus Lektion 4 diesen Status prüft, schlägt er jetzt fehl. Prüfe, was die Header enthalten, wie Lektion 4 es verlangt hat. header_value bleibt in redirect(), für jeden anderen Aufrufer und für den Code, den du nächsten Monat schreibst.

Ein korrektes Passwort kommt mit next=https://other.example an. Was sollte der Server tun?

Deine Aufgabe

In reading_server.py:

  1. Schreib safe_redirect_target(value) mit den vier Regeln, in dieser Reihenfolge. Prüfe den Anfang mit : value[:1] ist das erste Zeichen, oder "" bei einem leeren Wert, und value[:2] sind die ersten beiden.

  2. Leite den Wert in login_response durch sie hindurch: redirect(303, safe_redirect_target(params["next"][0])). Behalte das try drumherum.

Führe python -m pytest -q aus. Es endet weiterhin mit 23 passed: Der Angriffstest für diese Schutzmaßnahme gehört zum Projekt in Lektion 8. Führe dann das Szenario noch einmal aus:

--- a redirect: ana logs in, and next names another site ---
POST /login?next=%2F%2Fother.example%2Fwelcome  -> 303
  location: /
the redirect stays on this site

Der erste Teil zeigt jetzt ebenfalls eine 303 mit location: /, und die Ergebniszeile sagt 2 of 4 attacks worked.

Klicke dann auf Check my work. Der dritte Teil des Szenarios holt eine Datei, die kein Cover ist. Darum geht es in der nächsten Lektion.

Aufgabe

Sorge dafür, dass das Login nur auf einen Pfad auf dieser Site weiterleitet.

In reading_server.py:

  • safe_redirect_target(value) gibt value unverändert zurück, wenn alle vier Regeln gelten, und "/" für jeden anderen . Es löst nie etwas aus und repariert nie einen Wert.

    1. Der Wert ist druckbares ASCII: value.isascii() and value.isprintable().

    2. Er enthält keinen Backslash.

    3. Er beginnt mit einem Schrägstrich und nicht mit zweien: value[:1] == "/" und value[:2] != "//".

    4. urlsplit(value) findet kein Schema und keinen Host: scheme und netloc sind beide "".

  • login_response(request, user) schreibt redirect(303, safe_redirect_target(params["next"][0])), weiterhin innerhalb seines try.

Führe python -m pytest -q und python pl_sec.py injection im aus.

Klicke dann auf Check my work. Es ruft safe_redirect_target() mit Werten auf, die du noch nicht gesehen hast: Pfade auf dieser Site, mit einer Query, einem Fragment, einem Doppelpunkt oder einem kodierten Leerzeichen, die unverändert zurückkommen müssen, und Werte, die einen anderen Host nennen, ein Schema tragen, einen Backslash, einen Tab, einen Zeilenumbruch oder einen Buchstaben außerhalb von ASCII enthalten oder nicht mit genau einem Schrägstrich beginnen, die alle als "/" zurückkommen müssen. Es ruft handle() mit eigenen Benutzern auf: Ein korrektes Login, dessen next eine andere Site nennt oder einen kodierten Zeilenumbruch enthält, muss mit 303 und location: / antworten und eine starten; ein sauberes next, ein Login ohne next und ein falsches Passwort müssen wie vorher antworten. Nichts aus deinem Terminal wird bewertet.