0%

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 und einen Body, der id=1 sagt. Alles, was die betrachtet, ist bei beiden gleich. Um sie zu unterscheiden, braucht der Server etwas, das seine eigene Seite hat und das die andere Seite nicht bekommen kann.

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 lesen, den dein Server in diese Seite schreibt. Die arbeitet jetzt für den Server.

Dieser Wert ist ein Token: ein zufälliger Text aus secrets.token_urlsafe(32), der Funktion, die auch deine erzeugt, hier mit 32 Bytes. Der Server speichert ihn in der Session und schreibt ihn in ein verstecktes Feld jedes Löschformulars. 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 ein gefälschtes Formular in eine 500 verwandeln. Kodiere also beide Seiten und vergleiche Bytes:

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- akzeptiert, wie in /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:

  1. Füge in start_session dem Session- "csrf": secrets.token_urlsafe(32) hinzu.

  2. Übergib im Zweig "/" von route session["csrf"] als drittes an render_page.

  3. Schreibe csrf_matches(session, sent) mit dem Vergleich oben. Schreibe den Aufruf als hmac.compare_digest(...).

  4. 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ßer csrf_matches(session, fields.get("csrf", "")) ist wahr.

  5. Nimm im Kommentar oben in tests/test_attacks.py deinen Test aus Lektion 5 von der der 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 ein Token, schreibe es in das Formular der Seite und lehne ein Löschformular ab, das es nicht trägt.

  1. Füge in start_session in reading_server.py dem Session- "csrf": secrets.token_urlsafe(32) hinzu.

  2. Rufe im Zweig "/" von route render_page(books, session["user"], session["csrf"]) auf.

  3. Schreibe csrf_matches(session, sent). Sie gibt hmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8")) zurück: True nur, wenn sent das Token dieser Session ist. Rufe compare_digest über das auf, geschrieben als hmac.compare_digest(...).

  4. Gib in delete_from_form, nachdem das Formular gelesen ist und bevor die id betrachtet wird, json_error(403, "the form did not carry this session's token") zurück, außer csrf_matches(session, fields.get("csrf", "")) ist wahr. Lies das Token nur aus dem Formular-Body.

  5. Entferne im Kommentar oben in tests/test_attacks.py test_attack_another_origin_cannot_delete_a_book aus 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-. Jedes muss das Buch an seinem Platz lassen und eine 403 bekommen. Eine Anmeldung, die mit dem einer Besucher-Session ankommt, muss mit einem neuen Token enden. An die Stelle von reading_server.hmac.compare_digest wird eine aufzeichnende gesetzt: Der Vergleich muss über sie laufen. Zuletzt wird dein Angriffstest aus Lektion 5 einmal gegen deinen eigenen Server ausgeführt und muss bestehen. Nichts aus deinem wird bewertet.