0%

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 los. Der Server antwortete mit 401, und beide Bücher blieben. Dasselbe Formular von 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:

  1. Was wanted enthält. Es ist ein . "kind" ist "form", "link", "image" oder "fetch". Dann kommen "url", "method" und optional "headers" und "body". Die erste Hälfte der lehnt 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.

  2. Wo dein cookies_to_send aufgerufen wird. Das passiert gegen Ende, mit top_level wahr für ein Formular und für einen Link. Die Cookies, die es zurückgibt, werden zum Header cookie.

  3. Wo origin hinzugefügt wird. Ein paar Zeilen darüber, bei jeder Anfrage, die kein GET oder HEAD ist. Lektion 7 verwendet diesen Header.

  4. Was send ist. 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, die reading_server.handle() im Speicher aufruft.

  5. 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 das None, 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 und dieselbe Site. 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 . Der Name stammt aus dem Jahr 2001, Jahre bevor Browser die Linie zwischen einer Site und einer Origin zogen, die du in Lektion 2 kennengelernt hast, und diese Anfrage hat die Site nie verlassen. „Cross-Origin“ würde sie besser beschreiben, aber der Name ist geblieben.

Warum funktioniert die Fälschung von notes.reading.example aus und nicht von other.example aus?

Was nie in Gefahr war

Die -Routen unter /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_site in tests/test_browser.py. Er besteht.

  • test_attack_another_origin_cannot_delete_a_book in tests/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 , wie in 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"}.

  1. Schreibe in tests/test_browser.py unter dem Kommentar zu Kapitel 8 test_samesite_stops_a_form_from_another_site(). Poste das Formular von steps.OTHER aus und sichere zu, dass "cookie" not in result["requests"][0]["headers"]. Führe python -m pytest -q tests/test_browser.py aus. Der Test besteht.

  2. Schreibe in tests/test_attacks.py unter dem Kommentar zu Kapitel 8 test_attack_another_origin_cannot_delete_a_book(). Poste das Formular von steps.NOTES aus. Sichere zu, dass "cookie" in result["requests"][0]["headers"], und dann, dass 1 in state["books"]. Führe python -m pytest -q aus. 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- bei einem Cross-Site-Formular-POST mitschickt, wo er fehlschlagen muss. Dein Angriffstest läuft gegen einen Server ohne Abwehr, wo er fehlschlagen muss, und gegen einen Server, der ein Formular von einer anderen ablehnt, wo er bestehen muss. Er muss außerdem visit.visit aufrufen und reading_server.handle über das erreichen. Nichts aus deinem wird bewertet.