Origins und Cross-Site-Anfragen · Übung
SameSite und der Header Origin
Geh zurück zur Ausgabe von python pl_sec.py cross-site-delete in Lektion 5. Jedes gefälschte Formular hatte eine Zeile, die bisher nichts verwendet hat:
origin: https://notes.reading.example
visit() fügt jeder Anfrage, die kein GET oder HEAD ist, einen Header origin hinzu, wie ein Browser es tut. Das gefälschte Formular hat also die ganze Zeit gesagt, woher es kam, und dein Server hat nie hingesehen.
Ein Header, den die Seite nicht setzen kann
Der Header ist lesenswert, weil es darauf ankommt, wer ihn schreibt. Der Browser schreibt Origin, und das Skript einer Seite darf ihn nicht setzen. visit() hält sich an diese Regel: Es lehnt ein wanted ab, das es versucht. Sieh es dir einmal an. In dieser Zeile gibt sich eine Seite auf dem Nachbarn als die Leseliste aus:
python -c "import visit; visit.visit('https://notes.reading.example', {'kind': 'fetch', 'method': 'POST', 'url': 'https://reading.example/logout', 'headers': {'Origin': 'https://reading.example'}}, [], print)"
Der
ValueError: a page cannot set the Origin header; the browser writes it
Ein Programm, das kein Browser ist, kann jede Origin schreiben, die es will. Es hat keinen Browser von Ana, der ihr
Exakt, aus einem Set
sample_state() enthält seit Beginn dieses Kapitels ein Set: state["trusted_origins"], mit einem Element, "https://reading.example". Die Prüfung stellt eine Frage, nämlich ob der in diesem Set ist. Bei einem Set von Texten ist in nur für einen Text wahr, der vom ersten bis zum letzten Zeichen einem Element gleicht.
Jeder lockerere Vergleich lässt jemanden herein. Hier sind vier, jeder mit einer
| Ein Vergleich, der richtig aussieht | Eine Origin, die er hereinlässt |
|---|---|
Der Wert beginnt mit https://reading.example | https://reading.example.other.example |
Der Wert enthält reading.example | https://notreading.example |
| Der Wert liegt auf derselben Site wie die Leseliste | https://notes.reading.example, der Nachbar selbst |
Der Host ist reading.example, egal mit welchem Schema | http://reading.example |
Ein Server akzeptiert eine Origin, die mit https://reading.example beginnt. Welche Origin kommt herein?
Ein Anfrage-origin_allowed(request, trusted) mit request["headers"].get("origin", []). Sie gibt False zurück, außer diese Liste hat genau einen Wert. Dann antwortet sie, ob der Wert in trusted liegt. Eine Anfrage mit zwei Origin-Headern wird abgelehnt, egal was der erste sagt.
Fehlend und null
Ein Browser sendet in manchen Fällen Origin: null: von einer Seite, die angewiesen wurde, keinen Referrer zu senden, aus einem Sandbox-Frame und aus einer Datei, die von der Festplatte geöffnet wurde. Ein alter oder ungewöhnlicher Client sendet gar keine Origin. Lehne beides ab. Die "null" ist nicht im Set, und ein fehlender Header ist eine Liste ohne Werte.
Diese Ablehnung hat einen Preis, und der Preis ist gewählt. Ein Werkzeug, das an diese Route postet, muss den Header senden. pl_sec.py tut es, und die Tests in tests/test_attacks.py senden ihn seit Beginn dieses Kapitels. Sie bindet außerdem ein späteres Kapitel. Wenn Kapitel 10 Sicherheits-Header hinzufügt, darf es nicht Referrer-Policy: no-referrer setzen, sonst käme das eigene Formular der Leseliste mit Origin: null an.
Das Modell schreibt immer eine echte Origin. Die null-Fälle stammen aus dem Fetch- und dem HTML-Standard und werden hier nicht gezeigt. Fetch fügt zwei weitere hinzu: eine Anfrage, die über eine andere Origin weitergeleitet wurde, und einen POST von einer https-Seite an eine http-Adresse. Ein älterer Header, Referer, trägt dieselbe Idee und hat mehr Arten zu fehlen.
Ein per Cookie autorisierter POST kommt mit Origin: null an. Was tut der Server?
Drei Abwehrmaßnahmen, zwei Angreifer
Das Löschformular hat jetzt drei Abwehrmaßnahmen. In diesem Kapitel sind zwei Angreifer aufgetreten: eine Seite auf other.example und eine Seite auf dem Nachbarn.
| Abwehr | Ein Formular von other.example | Ein Formular von notes.reading.example |
|---|---|---|
SameSite=Lax | gestoppt: das Cookie bleibt zu Hause | nicht gestoppt: dieselbe Site, also geht das Cookie mit |
| Das Token im Formular | gestoppt: es kann die Seite nicht lesen | gestoppt: es kann die Seite nicht lesen |
Die Origin-Prüfung | gestoppt: nicht im Set | gestoppt: nicht im Set |
Behalte alle drei, weil jede auf eine andere Weise versagt: ein Browser ohne SameSite, ein Fehler in der Seite, der das Token preisgibt, eine Route, die jemand vergisst. Ein anderer Python-Land-Kurs, Origin-Prüfung, um einen WebSocket-Handshake zu schützen.
Welche Abwehr stoppt ein gefälschtes Formular von notes.reading.example nicht?
Deine Aufgabe
Schreibe
origin_allowed(request, trusted)inreading_server.py: genau einorigin-Wert, und dieser Wert im Set.Frage sie im Zweig
"/books/delete"vonroutebei einem POST vor allem anderen. Wennorigin_allowed(request, state["trusted_origins"])falsch ist, ist die Antwortjson_error(403, "this request did not come from a page of this service"), und diewird nicht nachgeschlagen.
Führe dann die Tests aus:
python -m pytest -q
Der Durchlauf endet weiterhin mit 30 passed. Kein Test schlägt ohne die neue Prüfung fehl, weil das Token das gefälschte Formular bereits stoppt. Das ist eine Lücke in den Tests, und das Projekt in Lektion 8 schließt sie.
Klicke dann auf Check my work.
Aufgabe
Prüfe den Header Origin des Löschformulars, exakt und gegen ein Set.
Schreibe in
reading_server.pyorigin_allowed(request, trusted). Liesrequest["headers"].get("origin", []), einevon . Gib Falsezurück, außer die Liste hat genau einen Wert. Gib dann zurück, ob dieser Wert im Settrustedliegt. Vergleiche den ganzen Text und repariere nichts: keine Kleinbuchstaben, kein entfernter Schrägstrich, kein Präfix.Rufe im Zweig
"/books/delete"vonroutebei einem POSTorigin_allowed(request, state["trusted_origins"])auf, bevor diegelesen wird. Wenn sie Falsezurückgibt, ist die Antwortjson_error(403, "this request did not come from a page of this service"). Andernfalls geht der Zweig weiter wie bisher.
Führe python -m pytest -q aus. Es endet mit 30 passed.
Klicke dann auf Check my work. Es ruft origin_allowed mit eigenen Anfrage-Origin-Headern, mit null, mit einem leeren Wert, mit einem Text, der nur ein Teil einer vertrauenswürdigen Origin ist, mit einem anderen Schema, mit einem Nachbar-Host derselben Site, mit einem vertrauenswürdigen Host ohne seinen Port, mit Doppelgängern, die wie eine vertrauenswürdige Origin beginnen oder enden, mit Großbuchstaben, mit einem Schrägstrich am Ende und mit einem bloßen Host. Dann meldet es einen eigenen Benutzer an und steuert deinen Server im Speicher, mit einem eigenen Browsermodell. Ein Formular von einem Nachbar-Host, das das richtige Token trägt, muss das Buch an seinem Platz lassen und eine 403 bekommen. Anfragen mit dem richtigen Origin, mit null oder mit einem Doppelgänger müssen eine 403 bekommen und nichts ändern. Eine Anfrage mit einer nicht vertrauenswürdigen Origin und ohne Cookie muss eine 403 bekommen und keine 401. Das eigene Formular der Seite muss weiterhin löschen. Nichts aus deinem