Origins und Cross-Site-Anfragen · Übung
Ein Token, das die andere Seite nicht kennen kann
Dein Angriffstest ist rot. Sieh dir an, was delete_from_form von der gefälschten Anfrage und von der ehrlichen zu sehen bekommt: einen POST für /books/delete, Anas id=1 sagt. Alles, was die
Ein Geheimnis in der Seite
Lektion 4 hat dir die Regel gegeben, die das möglich macht. Eine Seite auf notes.reading.example darf Anfragen an die Leseliste senden, und sie darf die Antworten nicht lesen. Sie kann also GET / nicht lesen, und sie kann keinen
Dieser Wert ist ein Token: ein zufälliger Text aus secrets.token_urlsafe(32), der Funktion, die auch deine page.html hat das Feld bereits, und render_page(books, user, token="") füllt es bereits, escapt wie jeden anderen Wert. Bisher hat niemand ein Token übergeben, also war das Feld leer. Wenn ein Formular hereinkommt, vergleicht der Server sein Feld mit dem Token in der Session.
the server the browser
session of ana holds the token T
GET / -> page with csrf = T -> page of reading.example
posts id=1&csrf=T -> T matches: 303
page of notes.reading.example
may not read that page
posts id=1 -> no token: 403
In Worten: Der Server hält das Token T in Anas Session und schreibt es in die Seite, die er ihr ausliefert. Die eigene Seite der Leseliste postet T mit dem Formular zurück, und das Buch wird gelöscht. Eine Seite auf dem Nachbarn kann die Seite der Leseliste nicht lesen. Ihr Formular trägt kein T, und der Server antwortet mit 403.
Warum kann eine Seite auf notes.reading.example nicht das richtige Token in ihr gefälschtes Formular setzen?
An die Session gebunden, bei der Anmeldung erneuert
Es gibt ein Token pro Session, und es entsteht dort, wo die Session entsteht: in start_session. Ein Token, das der Server für jede beliebige Session akzeptierte, wäre nichts wert. Der Angreifer hat auch eine Session und kann ein Token in einer eigenen Seite lesen.
Das Token dort zu erzeugen hat eine zweite Wirkung. In Kapitel 4 hast du rotate_session geschrieben, damit eine Anmeldung die alte Session beendet und eine neue startet und dabei die Seitengröße kopiert und sonst nichts. Eine Anmeldung bekommt also ein neues Token, ohne eine einzige weitere Codezeile. Der Grund ist der, den du von der Session-ID kennst: Ein Token, das die Anmeldung überlebte, wäre ein Token, das der Inhaber der Besucher-Session bereits kannte.
Ein Server akzeptiert jedes Token, das in irgendeiner Session existiert. Was kann Ben tun?
Vergleiche es wie ein Geheimnis
Das Token ist ein Geheimnis, das der Server mit einem erwarteten Wert vergleicht, also läuft der Vergleich über hmac.compare_digest, wie in Kapitel 2. Ein Detail ist neu. Das Feld enthält, was auch immer der Client gesendet hat, und compare_digest lehnt Text ab, der kein ASCII ist:
hmac.compare_digest("é", "abc")
TypeError: comparing strings with non-ASCII characters is not supported
In deinem Server würde diese
hmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8"))
Ein Formular ohne das Feld wird als leerer Text verglichen, fields.get("csrf", ""), und scheitert wie jedes andere falsche Token.
Die Ablehnung ist eine 403 und keine 401. Der Server weiß, wessen Session das ist, und eine erneute Anmeldung würde nicht helfen. Er lehnt diese eine Anfrage ab.
Eine angemeldete Session postet das Formular ohne Token. Welcher Status?
Nur aus dem Formular-Body
Lies das Token aus dem Formular-Body und von nirgendwo sonst. Ein Token, das der Server auch im Query-/books/delete?csrf=..., landet in Logs, im Verlauf des Browsers und in Links, die Leute weitergeben.
Warum wird ein Token in ?csrf=... nicht akzeptiert?
Deine Aufgabe
Nimm fünf Änderungen vor. Vier davon sind in reading_server.py:
Füge in
start_sessiondem Session-"csrf": secrets.token_urlsafe(32)hinzu.Übergib im Zweig
"/"vonroutesession["csrf"]als drittesan render_page.Schreibe
csrf_matches(session, sent)mit dem Vergleich oben. Schreibe den Aufruf alshmac.compare_digest(...).Gib in
delete_from_form, nachdem das Formular gelesen ist,json_error(403, "the form did not carry this session's token")zurück, außercsrf_matches(session, fields.get("csrf", ""))ist wahr.Nimm im Kommentar oben in
tests/test_attacks.pydeinen Test aus Lektion 5 von derder Tests, die erwartet fehlschlagen.
Führe dann die Tests aus:
python -m pytest -q
Der Durchlauf endet mit 30 passed: Das gefälschte Formular trägt kein Token, und das Buch bleibt.
Das Token hat Grenzen. Es schützt das Formular dieser Seite, und es hilft nicht, wenn das Skript eines Angreifers in der Seite selbst läuft. Ein solches Skript draußen zu halten ist die Aufgabe des Escapens, das du in Kapitel 7 geschrieben hast. Zwei Varianten haben Namen, denen du begegnen kannst, Tokens pro Anfrage und Double-Submit-Cookies, und dieser Kurs lehrt keine davon.
Eine weitere Fälschung verdient ein paar Sätze: eine gefälschte Anmeldung. Eine Seite, die Anas Browser mit den eigenen Zugangsdaten des Angreifers anmelden könnte, würde sie als den Angreifer anmelden. Hier braucht eine Anmeldung einen Basic-Header mit dem eigenen Namen und Passwort des Angreifers, und ein Formular kann diesen Header nicht setzen. Nach den Standards kann man einen Browser trotzdem dazu bringen, einen zu senden: Ein Formular, dessen Adresse einen Benutzernamen und ein Passwort trägt, etwa https://ben:pw@reading.example/login, darf die 401 des Servers damit beantworten, und manche Browser warnen vorher. Das Projekt in Lektion 8 setzt die Origin-Prüfung auf /login, und diese Prüfung lehnt eine solche Anfrage so oder so ab. Lies das nicht als „Basic-Authentifizierung kann man nicht fälschen“: Sobald ein Browser seinen Benutzer nach Basic-Zugangsdaten gefragt hat, merkt er sie sich und hängt sie an spätere Anfragen an, wie ein Cookie.
Klicke dann auf Check my work.
Aufgabe
Gib jeder
Füge in
start_sessioninreading_server.pydem Session-"csrf": secrets.token_urlsafe(32)hinzu.Rufe im Zweig
"/"vonrouterender_page(books, session["user"], session["csrf"])auf.Schreibe
csrf_matches(session, sent). Sie gibthmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8"))zurück:Truenur, wennsentdas Token dieser Session ist. Rufecompare_digestüber dasauf, geschrieben als hmac.compare_digest(...).Gib in
delete_from_form, nachdem das Formular gelesen ist und bevor dieidbetrachtet wird,json_error(403, "the form did not carry this session's token")zurück, außercsrf_matches(session, fields.get("csrf", ""))ist wahr. Lies das Token nur aus dem Formular-Body.Entferne im Kommentar oben in
tests/test_attacks.pytest_attack_another_origin_cannot_delete_a_bookaus den Tests, die erwartet fehlschlagen.
Führe python -m pytest -q aus, bis es mit 30 passed endet.
Klicke dann auf Check my work. Es meldet eigene Benutzer an und steuert deinen Server im Speicher, mit einem eigenen Browsermodell. Eine neue Session muss ein Token von mindestens 22 Zeichen enthalten, das aus secrets stammt, über das Modul aufgerufen, und zwei Sessions müssen verschiedene Tokens enthalten. Die Seite von GET / wird mit Pythons HTML-Parser gelesen: Jedes Löschformular muss das Token der Session in seinem Feld csrf tragen, und das eigene Formular der Seite muss weiterhin löschen. Dann werden Formulare von einem Nachbar-Host gepostet, den die Prüfung zu state["trusted_origins"] hinzugefügt hat: mit einem falschen Token, dem Token einer anderen Session, ohne Token, mit einem leeren, mit einem Token, das kein ASCII ist, und mit dem richtigen Token im Query-reading_server.hmac.compare_digest wird eine aufzeichnende