0%

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 endet mit dieser Zeile:

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 anhängt, also bringt ihm die Lüge nichts.

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 des Headers 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 , die ihn besteht:

Ein Vergleich, der richtig aussiehtEine Origin, die er hereinlässt
Der Wert beginnt mit https://reading.examplehttps://reading.example.other.example
Der Wert enthält reading.examplehttps://notreading.example
Der Wert liegt auf derselben Site wie die Leselistehttps://notes.reading.example, der Nachbar selbst
Der Host ist reading.example, egal mit welchem Schemahttp://reading.example

Ein Server akzeptiert eine Origin, die mit https://reading.example beginnt. Welche Origin kommt herein?

Ein Anfrage- hält jeden Header als von Werten, weil ein Client denselben Header zweimal senden kann. Also beginnt 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 oben tut das bereits: "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.

AbwehrEin Formular von other.exampleEin Formular von notes.reading.example
SameSite=Laxgestoppt: das Cookie bleibt zu Hausenicht gestoppt: dieselbe Site, also geht das Cookie mit
Das Token im Formulargestoppt: es kann die Seite nicht lesengestoppt: es kann die Seite nicht lesen
Die Origin-Prüfunggestoppt: nicht im Setgestoppt: 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, with Python, verwendet dieselbe Origin-Prüfung, um einen WebSocket-Handshake zu schützen.

Welche Abwehr stoppt ein gefälschtes Formular von notes.reading.example nicht?

Deine Aufgabe

  1. Schreibe origin_allowed(request, trusted) in reading_server.py: genau ein origin-Wert, und dieser Wert im Set.

  2. Frage sie im Zweig "/books/delete" von route bei einem POST vor allem anderen. Wenn origin_allowed(request, state["trusted_origins"]) falsch ist, ist die Antwort json_error(403, "this request did not come from a page of this service"), und die wird 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.

  1. Schreibe in reading_server.py origin_allowed(request, trusted). Lies request["headers"].get("origin", []), eine von . Gib False zurück, außer die Liste hat genau einen Wert. Gib dann zurück, ob dieser Wert im Set trusted liegt. Vergleiche den ganzen Text und repariere nichts: keine Kleinbuchstaben, kein entfernter Schrägstrich, kein Präfix.

  2. Rufe im Zweig "/books/delete" von route bei einem POST origin_allowed(request, state["trusted_origins"]) auf, bevor die gelesen wird. Wenn sie False zurückgibt, ist die Antwort json_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- und einem eigenen vertrauenswürdigen Set auf, das zwei enthält, eine davon mit . Die beiden müssen erlaubt werden. Diese müssen abgelehnt werden: eine Anfrage ohne den Header, mit zwei 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 und Token und ohne 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 wird bewertet.