Origins und Cross-Site-Anfragen · Übung
Eine Anfrage von einer Nachbar-Site fälschen
Lektion 4 endete mit einem Formular, das eine Seite auf einem Nachbar-Host an deinen Server postet. Diese Lektion sendet es. Starte das Szenario:
python pl_sec.py cross-site-delete
Es startet deinen Server auf diesem Computer und spielt mit deinem mini_browser.py einen Browser. Seine ersten Zeilen sagen, dass die Namen nur im Modell existieren und dass jedes Byte an diesen Computer geht. Ana meldet sich von der eigenen Seite der Leseliste aus an. Dann posten zwei andere Seiten das Löschformular für Buch 1:
--- a page on https://other.example posts the form: id=1 to /books/delete ---
cookie sent: no
origin: https://other.example
the server answered: 401 (pl_sec sees this; the page does not)
book ids left: 1, 2
--- a page on https://notes.reading.example posts the same form ---
cookie sent: yes
origin: https://notes.reading.example
the server answered: 303 (pl_sec sees this; the page does not)
book ids left: 2
Das Formular von other.example ging ohne Anas notes.reading.example trug das Cookie. Der Server antwortete mit 303, so wie er seiner eigenen Seite antwortet, und Buch 1 ist weg. Im letzten Teil der Ausgabe postet die eigene Seite der Leseliste ihr Formular, und das funktioniert auch. Dein Server kann das zweite Formular nicht vom dritten unterscheiden.
Keine der beiden Seiten hat einen Status gesehen. pl_sec.py gibt ihn aus, weil es den Browser spielt. Eine Seite, die ein Formular postet, liest die Antwort nie.
visit.py lesen
Öffne visit.py. Die Datei ist mitgeliefert, und du änderst sie nie. visit(page_origin, wanted, jar, send) stellt eine Anfrage so, wie ein Browser es täte, und fragt bei jeder Entscheidung deine Regeln. Lies sie auf fünf Dinge hin:
Was
wantedenthält. Es ist ein. "kind"ist"form","link","image"oder"fetch". Dann kommen"url","method"und optional"headers"und"body". Die erste Hälfte derlehnt ab, was eine Art nicht kann. Ein Formular sendet GET oder POST, und der einzige Header, den es setzen kann, ist ein Formular-Content-Type. Wo dein
cookies_to_sendaufgerufen wird. Das passiert gegen Ende, mittop_levelwahr für ein Formular und für einen Link. Die Cookies, die es zurückgibt, werden zum Headercookie.Wo
originhinzugefügt wird. Ein paar Zeilen darüber, bei jeder Anfrage, die kein GET oder HEAD ist. Lektion 7 verwendet diesen Header.Was
sendist. Eine Funktion, die du übergibst, so wie du in Networks with Python Funktionen herumgereicht hast. Sie trägt eine Anfrage zu ihrem Server und gibt die Antwort zurück.steps.send_to(state)baut eine, diereading_server.handle()im Speicher aufruft.Was zurückkommt. Wieder ein Dict.
"requests"listet jede Anfrage auf, die den Browser verlassen hat."response"ist das, was das Skript der Seite lesen darf. Bei einem Formular ist dasNone, und"blocked"sagt, warum.
Überspringe den Block, der mit Kapitel 9 markiert ist.
Was ist send in visit(page_origin, wanted, jar, send)?
Kapitel 4 hat schon einen Angreifer gestoppt
Das erste Ergebnis des Szenarios verdient einen Test. Öffne tests/test_browser.py. Jeder Test dieser Lektion beginnt gleich: Ana meldet sich von der eigenen Seite der Leseliste aus an, damit der Speicher ihr Cookie enthält.
state = reading_server.sample_state()
jar = []
send = steps.send_to(state)
wanted = {
"kind": "fetch",
"method": "POST",
"url": steps.OWN + "/login",
"headers": {"Authorization": helpers.basic("ana", "correct horse")},
}
result = visit.visit(steps.OWN, wanted, jar, send)
assert result["response"]["status"] == 200
assert len(jar) == 1
Dann postet eine Seite auf einer anderen Site das Formular:
form = {
"kind": "form",
"method": "POST",
"url": steps.OWN + "/books/delete",
"headers": {"Content-Type": "application/x-www-form-urlencoded"},
"body": b"id=1",
}
result = visit.visit(steps.OTHER, form, jar, send)
Schreibe test_samesite_stops_a_form_from_another_site aus diesen zwei Teilen und beende ihn mit der Zusicherung, dass "cookie" not in result["requests"][0]["headers"]. Er besteht beim ersten Durchlauf. Das Flag SameSite=Lax aus Kapitel 4 hält das Cookie zu Hause, und dein Modell zeigt es.
Bei einem echten Angriff öffnet Ana die andere Seite, während sie angemeldet ist. Hier spielt der Test ihre Rolle: Er meldet sie an und postet dann das Formular für sie.
Der Nachbar
notes.reading.example ist ein Host, auf dem Leser eigene Seiten veröffentlichen, also kann dort jeder ein Formular ablegen. Es ist eine andere Lax lässt das Cookie mitgehen, und Strict würde es auch.
Schreibe den Angriffstest in tests/test_attacks.py und nenne ihn test_attack_another_origin_cannot_delete_a_book. Melde Ana an, poste dasselbe Formular von steps.NOTES aus und sichere zwei Dinge in dieser Reihenfolge zu. Erstens, dass "cookie" in den Headern der Anfrage steht. Ohne diese Zeile könnte der Test bestehen, weil das Cookie zu Hause geblieben ist. Zweitens, dass 1 in state["books"]. Der Test darf state lesen. Die fälschende Seite nicht.
Führe alles aus:
python -m pytest -q
Die letzte Zeile beginnt mit 1 failed, 29 passed, und der Fehlschlag ist deine zweite Zusicherung: Buch 1 ist weg. Der Test tut seine Arbeit. Ein Angriffstest schlägt fehl, solange der Angriff funktioniert. Der Kommentar oben in der Datei führt diesen Test bereits als erwartet fehlschlagend bis Lektion 6.
Der Angriff heißt
Warum funktioniert die Fälschung von notes.reading.example aus und nicht von other.example aus?
Was nie in Gefahr war
Die /books sehen sich das Cookie nicht an. Sie verlangen ein Bearer-Token in einem Header Authorization. Ein Formular kann diesen Header nicht setzen, und kein Browser hängt von sich aus ein Token an. Dieselbe Seite kann also kein DELETE /books/1 fälschen. Die Fälschung lebt dort, wo ein Cookie der Berechtigungsnachweis ist.
Warum kann dieselbe Seite auf der JSON-Route kein DELETE /books/1 fälschen?
Deine Aufgabe
Schreibe die zwei Tests, jeden unter dem Kommentar zu Kapitel 8 seiner Datei:
test_samesite_stops_a_form_from_another_siteintests/test_browser.py. Er besteht.test_attack_another_origin_cannot_delete_a_bookintests/test_attacks.py. Seine letzte Zusicherung schlägt heute fehl. Die nächste Lektion ist die erste, die verlangt, dass er besteht.
Du änderst in dieser Lektion nichts in reading_server.py. Beide Tests erreichen den Code über sein visit.visit(...) und reading_server.sample_state(), ohne eine Zeile from ... import.
Klicke dann auf Check my work.
Aufgabe
Schreibe zwei Tests: einen, der SameSite=Lax bei der Arbeit zeigt, und einen, der eine Anfrage von einem Nachbar-Host fälscht und fehlschlägt.
Beide Tests melden zuerst ana über das Modell an: jar = [], send = steps.send_to(state) mit state = reading_server.sample_state() und visit.visit(steps.OWN, wanted, jar, send) mit einem wanted, dessen "kind" "fetch" ist, dessen "method" "POST" ist, dessen "url" steps.OWN + "/login" ist und dessen "headers" "Authorization": helpers.basic("ana", "correct horse") enthalten. Sichere zu, dass result["response"]["status"] == 200.
Beide posten dann das Löschformular mit visit.visit(page, form, jar, send). Das Formular ist {"kind": "form", "method": "POST", "url": steps.OWN + "/books/delete", "headers": {"Content-Type": "application/x-www-form-urlencoded"}, "body": b"id=1"}.
Schreibe in
tests/test_browser.pyunter dem Kommentar zu Kapitel 8test_samesite_stops_a_form_from_another_site(). Poste das Formular vonsteps.OTHERaus und sichere zu, dass"cookie" not in result["requests"][0]["headers"]. Führepython -m pytest -q tests/test_browser.pyaus. Der Test besteht.Schreibe in
tests/test_attacks.pyunter dem Kommentar zu Kapitel 8test_attack_another_origin_cannot_delete_a_book(). Poste das Formular vonsteps.NOTESaus. Sichere zu, dass"cookie" in result["requests"][0]["headers"], und dann, dass1 in state["books"]. Führepython -m pytest -qaus. Dieser Test schlägt bei seiner letzten Zusicherung fehl: Das Buch ist weg. Bestehen muss er erst in der nächsten Lektion.
Ändere nichts in reading_server.py.
Klicke dann auf Check my work. Es wählt die zwei Tests namentlich aus und führt jeden ein paarmal im Speicher aus. Dein Test des Modells läuft mit einem Modell, dessen Regeln stimmen, wo er bestehen muss, und mit einem Modell, das ein Lax-visit.visit aufrufen und reading_server.handle über das