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_neighborfügt den Nachbarn vor allem anderen dem vertrauenswürdigen Set hinzu. DieOrigin-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_neighborliest Anas Token aus dem State, was keine Seite kann, mitstate["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
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 POST /v0/books ab, das seit
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 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
Vier Tests in
tests/test_attacks.pyund einer intests/test_browser.py, die ersten beiden davon einmal fehlschlagend gesehen.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 inCOOKIE_POSTSfür dich aus.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
--- 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
Zeile F12 von
SECURITY.md, in deinen eigenen Worten. Ihre Test-Zelle nennt die vier Angriffstests dieses Kapitels. Ihr Status istopen: 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
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 state["sessions"][jar[0]["value"]]["csrf"]. Das Formular ist das Löschformular aus Lektion 5, gepostet mit visit.visit(page, form, jar, send).
Schreibe in
tests/test_attacks.pyunter deinem Test aus Lektion 5 vier Tests.test_attack_token_alone_stops_the_neighbor(): zuerststate["trusted_origins"].add(steps.NOTES). Poste das Formular mit dem Bodyb"id=1"vonsteps.NOTESaus. Sichere zu, dass"cookie"in den Headern der Anfrage steht und dass1 in state["books"].test_attack_origin_check_alone_stops_the_neighbor(): Poste das Formular mit dem Body("id=1&csrf=" + token).encode("utf-8")vonsteps.NOTESaus und lassstate["trusted_origins"]in Ruhe. Sichere dieselben zwei Dinge zu.test_attack_a_get_cannot_delete_a_book(): Folge einem Link vonsteps.OTHERaus,{"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 vonsteps.OWNaus und sichere zu, dass1 not in state["books"].
Schreibe in
tests/test_browser.pytest_samesite_alone_does_not_stop_the_neighbor(): Poste das Formular mit dem Bodyb"id=1"vonsteps.NOTESaus und sichere zu, dass"cookie"in den Headern der Anfrage steht.Mache in
routeinreading_server.pydies zur ersten Anweisung, nachdemsegmentsgesetzt ist: Wennmethod == "POST",segments in COOKIE_POSTSundorigin_allowed(request, state["trusted_origins"])falsch ist, gibjson_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.Führe
python -m pytest -qaus, bis es mit35 passedendet, und dannpython pl_sec.py cross-site-delete.Fülle in
SECURITY.mdZeile F12. Ihre Test-Zelle nennttest_attack_another_origin_cannot_delete_a_bookund die dreitest_attack_-Tests dieser Lektion. Ihr Status istopen: 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 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 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