0%

CORS · Übung

Cookies über Origins hinweg

Das Frontend kann die Buchliste lesen, sobald es ein Token hat. Bisher hat ihm ein Test eines gegeben. Eine echte Seite muss ihr Token irgendwoher bekommen.

POST /tokens gibt einer angemeldeten ein Token, und die Session lebt in einem von reading.example. Die Seite des Frontends muss also eine Anfrage an einen anderen mit anas Cookie senden und die Antwort lesen. Beide Teile brauchen eine Regel, die das Modell noch nicht hat.

Ein Fetch lässt Cookies zu Hause, außer man bittet darum

In Kapitel 8 hat jede Anfrage den Cookie-Speicher berücksichtigt. Für diese Anfragen war das richtig: Ein Formular, ein Link und ein einfaches Bild tun das immer. Der Fetch eines Skripts tut das nur für seinen eigenen Origin. Für einen anderen Origin lässt er jedes Cookie zu Hause, außer die Seite verlangt Zugangsdaten (credentials). Im Modell ist das "credentials": True im wanted.

attaches_cookies(kind, url, page_origin, *, credentials) formuliert diese Regel: True für jede Art außer "fetch", True für einen Fetch an den eigenen Origin der Seite, und sonst das, was credentials sagt. Es beantwortet, ob Cookies überhaupt berücksichtigt werden. Welche Cookies dann mitgehen, bleibt Sache von cookies_to_send.

Ein Skript auf https://app.reading.example ruft per Fetch https://reading.example/me ab und verlangt nichts Besonderes. Geht das Session-Cookie mit?

Eine strengere Erlaubnis

Eine Antwort auf eine Anfrage, die ein Cookie getragen hat, kann enthalten, was nur der angemeldete Leser sehen sollte. Deshalb verlangt ein Browser mehr vom Server, bevor eine Seite sie lesen darf:

  • Access-Control-Allow-Origin muss den exakten Origin der Seite nennen. Ein * wird nicht akzeptiert.

  • Die Antwort muss außerdem Access-Control-Allow-Credentials: true sagen, genau in dieser Schreibweise.

may_read bekommt diese Regel anstelle des return False aus Lektion 2. Dasselbe gilt für einen Preflight, der vor einer Anfrage mit Zugangsdaten kommt. preflight_allows bekommt ein Nur-Schlüsselwort-, credentials=False. Es gibt den an may_read weiter, und wenn er wahr ist, zählt ein * in der der oder in der Liste der Header nichts.

Auf dem Server bekommt cors_headers dasselbe Schlüsselwort. Mit credentials=True bekommt ein erlaubter Origin außerdem access-control-allow-credentials: true. Ein Origin, der nicht erlaubt ist, bekommt keinen der beiden Header.

Eine Antwort auf einen Fetch mit Zugangsdaten trägt Access-Control-Allow-Origin: * und Access-Control-Allow-Credentials: true. Darf die Seite sie lesen?

Folge dem Token

Hier ist das POST /tokens des Frontends, eine Entscheidung nach der anderen.

  1. Die Anfrage hat keinen Body und keinen eigenen Header der Seite. Sie ist eine einfache Anfrage, also wird kein Preflight gesendet.

  2. Die Seite hat Zugangsdaten verlangt, also sagt attaches_cookies Ja. Das Session-Cookie ist SameSite=Lax, und die beiden Hosts sind eine Site, also lässt cookies_to_send es gehen.

  3. Der Browser fügt Origin: https://app.reading.example hinzu. Dein Server prüft diesen Header auf dieser Route seit Kapitel 8, und heute würde er ablehnen. Der Origin des Frontends muss in state["trusted_origins"] aufgenommen werden.

  4. Der Server erstellt ein Token. cors_headers nennt mit credentials=True den Origin des Frontends und erlaubt Zugangsdaten.

  5. may_read findet den exakten Origin und true, und die Seite liest ihr Token.

Eine Seite auf notes.reading.example kann dasselbe POST senden. Sie liegt auf derselben Site und darf ebenfalls Zugangsdaten verlangen, also geht anas Cookie mit. Die Prüfung von Origin lehnt sie mit 403 ab, bevor ein Token erstellt wird. Und die Seite hätte eine Antwort ohnehin nicht lesen können, weil keine Antwort ihren Origin nennt.

Eine Seite auf notes.reading.example macht dasselbe POST /tokens mit Zugangsdaten. Was hindert sie daran, ein Token zu bekommen?

Ein Set

Nach Schritt 3 würde dein Zustand dieselbe Entscheidung zweimal enthalten: cors_origins für das Lesen, trusted_origins für die Prüfung von Origin. Zwei Sets, die übereinstimmen müssen, werden eines Tages nicht übereinstimmen. Lösche cors_origins aus sample_state(), nimm den Origin des Frontends in trusted_origins auf und übergib dieses eine Set an jeden Aufruf von cors_headers.

Der eigene Origin des Dienstes steht jetzt in dem Set, das verwendet. Das ändert nichts, weil eine Seite nie CORS braucht, um ihren eigenen Origin zu lesen.

Das Frontend hat auch etwas dazugewonnen. Jedes POST, das die Prüfung von Origin bewacht, nimmt es jetzt an, also kann seine Seite POST /logout senden und ana abmelden. Einem Origin, den du mit Zugangsdaten lesen lässt, vertraust du die Session an.

Vertrauen hat eine Grenze. Das Frontend kann das Löschformular trotzdem nicht absenden: POST /books/delete verlangt das Token von der eigenen Seite der Leseliste, und die Seite des Frontends hat es nicht.

Nur zwei Routen antworten mit Zugangsdaten: GET /me und POST /tokens. Die Buch-Routen brauchen keine. Ein Bearer-Token, das ein Skript in einen Header setzt, ist im Sinne des Browsers kein Zugangsdatum, und dort wird kein Cookie gelesen.

Lies den Rest von visit.py

Öffne visit.py und finde den Block, der mit Kapitel 9 markiert ist. Seine neun Zeilen rufen vier auf, und du hast alle geschrieben. Ab jetzt trägt visit() einen Fetch an einen anderen Origin von Anfang bis Ende.

Deine Aufgabe

In mini_browser.py: Schreibe attaches_cookies und erweitere may_read und preflight_allows.

In reading_server.py: Füge cors_headers das Schlüsselwort hinzu, nimm die Änderung an sample_state() vor und übergib state["trusted_origins"] in den drei Aufrufen, die du hast. Füge dann dem Zweig "/me" im Fall für GET und HEAD eine Zeile hinzu, und dieselbe Zeile dem Zweig "/tokens" im Fall für POST:

            response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True))

Sieh dir dann den ganzen Austausch auf an:

python -m pytest -q
python pl_sec.py cors

Die Tests enden mit 37 passed. Das Szenario spielt das Frontend, dann den Nachbarn, dann eine andere Site. Das ist die erste Anfrage des Frontends und die letzte des Nachbarn:

--- the front end, https://app.reading.example: POST /tokens, and it asks for credentials ---
  sent: POST /tokens   cookie: yes   token: no   origin: https://app.reading.example
  your server answered 200
        access-control-allow-origin: https://app.reading.example
        access-control-allow-credentials: true
        vary: Origin
  the page may read the response: yes
  the page now holds a token (not shown)
  sent: GET /me   cookie: yes   token: no   origin: https://notes.reading.example
  your server answered 200
        vary: Origin
  the page may read the response: no (the response did not give this origin permission to read it)

Die letzte Zeile des Ergebnisses sagt the front end read 4 of 4 responses; pages on other origins read 0 of 4; the request without an Origin header was answered 200.

Das Modell kennt nur Cookies. Im Standard umfassen Zugangsdaten außerdem eine Anmeldung, die sich der Browser für die -Authentifizierung merkt, und Client-. Und das Frontend hat Glück, dass es eine Site mit dem Dienst teilt. Ein Frontend auf einer anderen Site bräuchte ein Session-Cookie mit SameSite=None, und das ist eine größere Entscheidung, als diese Lektion trifft. Selbst dann kann ein Browser das Cookie zurückhalten. Safari blockiert Cookies, die an Dritte gehen, Firefox hält für jede Site einen eigenen Cookie-Speicher, und Chrome sendet sie, außer der Benutzer hat das abgeschaltet oder ist in einem Inkognito-Fenster.

Klicke dann auf Check my work.

Aufgabe

Bring dem Modell bei, wann ein Fetch trägt und was eine Seite dann lesen darf, und lass den Server das auf zwei Routen erlauben.

In mini_browser.py:

  • attaches_cookies(kind, url, page_origin, *, credentials) gibt True zurück, wenn kind nicht "fetch" ist, True, wenn same_origin(url, page_origin), und sonst den von credentials.

  • may_read(response, page_origin, *, credentials): ohne Zugangsdaten wie in Lektion 2. Mit Zugangsdaten gibt es nur dann True zurück, wenn access-control-allow-origin gleich page_origin ist und access-control-allow-credentials genau "true" ist. Ein * gibt dann keine Erlaubnis.

  • preflight_allows(response, page_origin, method, headers, *, credentials=False) gibt credentials an may_read weiter. Wenn credentials wahr ist, zählt ein * in access-control-allow-methods oder in access-control-allow-headers nichts.

In reading_server.py:

  • cors_headers(request, trusted, *, credentials=False): Für einen erlaubten , und nur dann, fügt credentials=True den Eintrag "access-control-allow-credentials": "true" hinzu.

  • In sample_state() enthält "trusted_origins" die Werte "https://reading.example" und "https://app.reading.example", und "cors_origins" ist weg. Die drei Aufrufe von cors_headers aus den Lektionen 2 und 4 übergeben state["trusted_origins"].

  • Im Zweig "/me", im Fall für GET und HEAD, und im Zweig "/tokens", im Fall für POST: response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True)). Keine andere Route erlaubt Zugangsdaten.

Führe python -m pytest -q, python mini_browser.py und python pl_sec.py cors aus.

Klicke dann auf Check my work. Es ruft attaches_cookies mit zehn Anfragen auf, die du noch nicht gesehen hast: ein Formular, ein Link, ein Bild und Fetches an den eigenen Origin der Seite, an einen anderen Origin und an einen anderen , mit und ohne Zugangsdaten. Es ruft may_read und preflight_allows mit eigenen Antworten auf: mit und ohne den Header für Zugangsdaten, mit einem Stern an jeder Stelle und mit anderen Schreibweisen von true. Es ruft cors_headers mit credentials=True für Origins innerhalb und außerhalb des Sets auf. Dann verwendet es einen eigenen Zustand, der kein cors_origins hat, meldet einen Benutzer an und sendet Anfragen an handle(): /me und /tokens vom Origin des Frontends und von zwei anderen, und neun andere Routen, von denen keine Zugangsdaten erlauben oder mit einem Stern antworten darf. Ein eigenes Browsermodell spielt das Frontend, einen Nachbarn und eine andere Site gegen deinen Server. Außerdem liest es sample_state()["trusted_origins"]. Nichts aus deinem wird bewertet.