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.
location macht. Er löst den Wert gegen die // 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.examplenennt einen Host, wie du gerade gesehen hast.https://other.example/ist eine ganze URL, mit Schema und Host./\other.examplebeginnt 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
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 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, //[, 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:
Nur druckbares ASCII:
value.isascii() and value.isprintable().isprintable()istFalsefür einen Tab, einen Zeilenumbruch und jedes andere Steuerzeichen. Die ASCII-Hälfte stoppt auch einen Buchstaben wieé, weilhttp_adapterHeader als Latin-1 schreibt und ein solches Zeichen nicht so ankäme, wie es gesendet wurde.Nirgends ein Backslash.
Ein Schrägstrich am Anfang, und nicht zwei.
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:
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, undvalue[:2]sind die ersten beiden.Leite den Wert in
login_responsedurch sie hindurch:redirect(303, safe_redirect_target(params["next"][0])). Behalte dastrydrumherum.
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)gibtvalueunverändert zurück, wenn alle vier Regeln gelten, und"/"für jeden anderen. Es löst nie etwas aus und repariert nie einen Wert. Der Wert ist druckbares ASCII:
value.isascii() and value.isprintable().Er enthält keinen Backslash.
Er beginnt mit einem Schrägstrich und nicht mit zweien:
value[:1] == "/"undvalue[:2] != "//".urlsplit(value)findet kein Schema und keinen Host:schemeundnetlocsind beide"".
login_response(request, user)schreibtredirect(303, safe_redirect_target(params["next"][0])), weiterhin innerhalb seinestry.
Führe python -m pytest -q und python pl_sec.py injection im
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 next, ein Login ohne next und ein falsches Passwort müssen wie vorher antworten. Nichts aus deinem Terminal wird bewertet.