0%

Origins und Cross-Site-Anfragen · Abschlussprojekt

Abschlussprojekt: Projekt: Anfragen von woanders

Das Löschformular hat jetzt zwei eigene Wächter, ein Token und eine Origin-Prüfung, und einen Angriffstest. Dieser Test kann nicht sagen, welcher Wächter die Fälschung gestoppt hat. Nimm einen der beiden weg, und er besteht immer noch.

Dieses Projekt beweist jede Abwehr für sich. Bewiesen hat hier eine schlichte Bedeutung: ein Test, der immer noch besteht, nachdem du dem Angreifer alles gegeben hast, was die andere Abwehr ihm vorenthält.

Eine Abwehr abschalten, indem man sie aushändigt

Dem Server wird kein Schalter hinzugefügt. Ein Test ändert, was der Angreifer in der Hand hat.

  • Das Token allein. test_attack_token_alone_stops_the_neighbor fügt den Nachbarn vor allem anderen dem vertrauenswürdigen Set hinzu. Die Origin-Prüfung lässt das gefälschte Formular jetzt durch. Das Formular trägt kein Token, und das Buch muss bleiben.

  • Die Origin-Prüfung allein. test_attack_origin_check_alone_stops_the_neighbor liest Anas Token aus dem State, was keine Seite kann, mit state["sessions"][jar[0]["value"]]["csrf"]. Er postet das Formular mit dem Body ("id=1&csrf=" + token).encode("utf-8"). Die Token-Prüfung besteht jetzt, und das Buch muss bleiben.

Der erste der beiden beginnt so:

def test_attack_token_alone_stops_the_neighbor():
    """The Origin check is handed to the attacker: the neighbor is trusted. The token alone must stop the forgery."""
    state = reading_server.sample_state()
    state["trusted_origins"].add(steps.NOTES)

Beide Tests posten von steps.NOTES aus. Beide sichern zu, dass das mitgegangen ist, bevor sie irgendetwas über das Buch zusichern, wie dein Test aus Lektion 5.

Dein Test fügt https://notes.reading.example zu state["trusted_origins"] hinzu und postet dann das gefälschte Formular ohne Token. Was beweist ein Bestehen?

Sieh jeden dieser zwei Tests einmal fehlschlagen. Setze return True als erste Zeile von csrf_matches, führe python -m pytest -q aus und nimm die Zeile wieder heraus. Mach dann dasselbe in origin_allowed. Jeder Durchlauf endet mit einem Fehlschlag, und es ist der Test für diese Abwehr.

Zwei weitere Tests vervollständigen das Bild. test_the_page_can_still_delete_its_own_book postet das Formular mit dem Token von steps.OWN aus und sichert zu, dass Buch 1 weg ist. Eine Abwehr, die auch die ehrliche Seite stoppt, hat den Dienst kaputt gemacht. In tests/test_browser.py steht test_samesite_alone_does_not_stop_the_neighbor neben deinem Test aus Lektion 5: Das Formular von steps.NOTES trägt das Cookie. Zusammen halten die beiden fest, was SameSite stoppt und was nicht.

Ein Ort für die Origin-Prüfung

Das Löschformular ist nicht der einzige POST, den ein Cookie autorisiert. Anmelden, Abmelden, die Seitengröße und ein neues Token lesen alle das Session-Cookie. Die Projektdatei führt die fünf auf:

COOKIE_POSTS = [["login"], ["logout"], ["prefs"], ["tokens"], ["books", "delete"]]

Nimm die Prüfung aus dem Lösch-Zweig heraus und mache sie zur ersten Anweisung von route(), nachdem segments gesetzt ist. Ein POST, dessen segments in COOKIE_POSTS liegen und dessen Origin nicht erlaubt ist, bekommt sofort die 403, mit einem .

Warum eine und keine Regel wie „jeder POST außer den Buchrouten“? Der Pfad des Löschformulars hat die Form eines Buchpfads, also lässt eine , die nach Form geschrieben ist, dieses Formular ungeprüft. Und eine Regel per Ausschluss lehnt POST /v0/books ab, das seit with Python mit 308 antwortet und ein Token nimmt, kein Cookie. Eine Liste hat auch ihren Preis: Sie weiß nichts von einer Route, die jemand später hinzufügt. Der Kommentar über der Liste sagt, was dann zu tun ist. Eine neue Route, die das Session-Cookie liest, kommt hinein.

Die Prüfung auf /login hat einen Preis, und er ist gewählt: Ein Kommandozeilen-Client, der keine Origin sendet, kann sich nicht mehr anmelden.

Kein GET ändert den Zustand

test_attack_a_get_cannot_delete_a_book folgt einem Link von einer anderen Site. Die des Links ist steps.OWN + "/books/delete?id=1&csrf=" + token:

result = visit.visit(steps.OTHER, {"kind": "link", "url": url}, jar, send)

Ein angeklickter Link ist ein Top-Level-GET, und ein Top-Level-GET, von einem Link oder von einem mit GET gesendeten Formular, ist die einzige Art von Cross-Site-Anfrage, die ein Lax-Cookie trägt. Sichere zu, dass das Cookie mitgegangen ist und dass das Buch geblieben ist. Dein Server antwortet heute mit 405, weil die Route nur POST erlaubt, und dieser Test sorgt dafür, dass es so bleibt.

Meilensteine

  1. Vier Tests in tests/test_attacks.py und einer in tests/test_browser.py, die ersten beiden davon einmal fehlschlagend gesehen.

  2. Die Prüfung am Anfang von route(). Deine Tests können diesen Schritt nicht sehen: Sie bestehen davor und danach. Check my work probiert die anderen Routen in COOKIE_POSTS für dich aus.

  3. Zwei Durchläufe: die Tests und das Szenario. Das Szenario ist zum Lesen für dich da und wird nicht bewertet.

python -m pytest -q
python pl_sec.py cross-site-delete

Der erste Durchlauf endet mit 35 passed. Im zweiten wird das Formular des Nachbarn jetzt abgelehnt, und ebenso das Formular von other.example, bevor eine nachgeschlagen wird:

--- a page on https://notes.reading.example posts the same form ---
  cookie sent:         yes
  origin:              https://notes.reading.example
  the server answered: 403   (pl_sec sees this; the page does not)
  book ids left:       1, 2

Die letzten Zeilen sagen:

--- result ---
0 of 2 forged forms deleted a book; the page's own form deleted its book
  1. Zeile F12 von SECURITY.md, in deinen eigenen Worten. Ihre Test-Zelle nennt die vier Angriffstests dieses Kapitels. Ihr Status ist open: the forgery half closed in 8.8, weil der Befund eine zweite Hälfte hat.

Was noch offen ist

Diese zweite Hälfte handelt vom Lesen. https://app.reading.example ist ein zweites Frontend der Leseliste. Seine Seiten sollten lesen dürfen, was dein Server antwortet, und heute stoppt die sie, wie sie jede andere stoppt. Kapitel 9 gibt dieser einen Origin die Erlaubnis zum Lesen.

Aufgabe

Beweise jede Abwehr für sich, setze die Origin-Prüfung für fünf Routen an einen Ort und halte den Befund fest.

Jeder Test meldet zuerst ana über das Modell an, wie in Lektion 5, und erreicht den Code über sein . Das Token ihrer ist state["sessions"][jar[0]["value"]]["csrf"]. Das Formular ist das Löschformular aus Lektion 5, gepostet mit visit.visit(page, form, jar, send).

  1. Schreibe in tests/test_attacks.py unter deinem Test aus Lektion 5 vier Tests.

    • test_attack_token_alone_stops_the_neighbor(): zuerst state["trusted_origins"].add(steps.NOTES). Poste das Formular mit dem Body b"id=1" von steps.NOTES aus. Sichere zu, dass "cookie" in den Headern der Anfrage steht und dass 1 in state["books"].

    • test_attack_origin_check_alone_stops_the_neighbor(): Poste das Formular mit dem Body ("id=1&csrf=" + token).encode("utf-8") von steps.NOTES aus und lass state["trusted_origins"] in Ruhe. Sichere dieselben zwei Dinge zu.

    • test_attack_a_get_cannot_delete_a_book(): Folge einem Link von steps.OTHER aus, {"kind": "link", "url": steps.OWN + "/books/delete?id=1&csrf=" + token}. Sichere dieselben zwei Dinge zu.

    • test_the_page_can_still_delete_its_own_book(): Poste das Formular mit dem Token von steps.OWN aus und sichere zu, dass 1 not in state["books"].

  2. Schreibe in tests/test_browser.py test_samesite_alone_does_not_stop_the_neighbor(): Poste das Formular mit dem Body b"id=1" von steps.NOTES aus und sichere zu, dass "cookie" in den Headern der Anfrage steht.

  3. Mache in route in reading_server.py dies zur ersten Anweisung, nachdem segments gesetzt ist: Wenn method == "POST", segments in COOKIE_POSTS und origin_allowed(request, state["trusted_origins"]) falsch ist, gib json_error(403, "this request did not come from a page of this service") zurück. Nimm die Prüfung aus dem Zweig "/books/delete" heraus.

  4. Führe python -m pytest -q aus, bis es mit 35 passed endet, und dann python pl_sec.py cross-site-delete.

  5. Fülle in SECURITY.md Zeile F12. Ihre Test-Zelle nennt test_attack_another_origin_cannot_delete_a_book und die drei test_attack_-Tests dieser Lektion. Ihr Status ist open: the forgery half closed in 8.8.

Klicke dann auf Check my work. Es meldet einen eigenen Benutzer an und steuert deinen Server im Speicher, mit einem eigenen Browsermodell. Jede der fünf Routen in COOKIE_POSTS bekommt einen POST mit einem gültigen , dessen Origin fehlt, null ist, ein Nachbar-Host oder eine andere Site: Jeder muss eine 403 bekommen und nichts ändern. Ein POST und ein PUT, die ein Token autorisiert, müssen weiterhin ohne Origin bedient werden, POST /v0/books muss weiterhin mit 308 antworten, und POST /me, /about und / müssen weiterhin mit 405 antworten. Die fünf Routen müssen von der eigenen des Dienstes aus weiterhin funktionieren. Vierundzwanzig GET- und HEAD-Anfragen dürfen nichts ändern. Die Prüfungen der Lektionen 6 und 7 laufen noch einmal mit neuen . Deine sieben Tests dieses Kapitels werden namentlich ausgewählt. Jeder muss gegen einen eigenen Server der Prüfung bestehen, der jede Abwehr hat, mit einem Modell, dessen Regeln stimmen. Jeder muss gegen den Server oder mit dem Modell fehlschlagen, der oder das genau den einen Fehler hat, für den der Test geschrieben ist: keine Token-Prüfung, keine Origin-Prüfung, keins von beiden, ein GET, das löscht, das Löschformular von jeder Seite abgelehnt, ein Lax-Cookie bei einem Cross-Site-Formular-POST und ein Cookie, das dem Nachbarn vorenthalten wird. Kein Test darf eine Verbindung öffnen. Zeile F12 wird als Tabellenzeile gelesen; ihre Formulierung wird nicht beurteilt. Nichts aus deinem wird bewertet.