0%

CORS · Abschlussprojekt

Abschlussprojekt: Projekt: CORS bei Server und Browser

Das Frontend kann jetzt seine ganze Arbeit erledigen, in vier Anfragen:

  1. POST /tokens mit Zugangsdaten, um ein Token für anas zu bekommen.

  2. GET /books mit dem Token, nach einem Preflight.

  3. PUT /books/1 mit dem Token und einem -Body, nach einem Preflight.

  4. GET /me mit Zugangsdaten, um anzuzeigen, wer angemeldet ist.

Noch nichts sagt, was eine Seite auf einem anderen nicht tun kann. Dieses Projekt schreibt diese Tests, und es verlegt die -Antwort des Servers an eine Stelle.

Vier Tests

Alle vier kommen in tests/test_attacks.py. Drei davon beginnen gleich, mit ana, die über die eigene Seite der Leseliste angemeldet ist:

    state = steps.keyed_state()
    jar = []
    send = ana_logs_in(state, jar)

Danach ist die Anfrage einer Seite visit.visit(page_origin, wanted, jar, send), und result["response"] ist das, was das Skript der Seite lesen darf, oder None.

test_the_front_end_can_work spielt die vier Anfragen oben von steps.APP aus und sichert jedes Mal zu, was die Seite gelesen hat: ein Token, einen Status 200, zwei Bücher, den Benutzer ana.

test_attack_another_origin_cannot_read_the_list ruft /books mit anas Token und /me mit Zugangsdaten von steps.NOTES und von steps.OTHER ab. Jede Antwort muss None sein. Zuerst sichert er zu, dass derselbe Abruf von /books von steps.APP aus funktioniert, damit der Test nicht gegen einen Server bestehen kann, auf dem niemand lesen kann.

test_attack_another_origin_cannot_get_a_token sendet das POST /tokens aus Lektion 5 von den beiden anderen Origins. Die Seite liest nichts, und das allein beweist zu wenig: Ein Server könnte ein Token erstellen und es bloß nicht zeigen. Eine Seite sieht nie eine Antwort, die sie nicht lesen darf. Ein Test darf das:

    result = visit.visit(steps.NOTES, wanted, jar, send)
    assert result["response"] is None
    assert "token" not in helpers.json_of(send(result["requests"][0]))

test_cors_does_not_stop_a_plain_client verwendet überhaupt keinen Browser. Er gibt GET /books mit einem Token und ohne Header Origin direkt an reading_server.handle und sichert 200 und zwei Bücher zu.

Die Tests bestehen, sobald du sie schreibst, weil Lektion 5 den Server korrekt hinterlassen hat. Sieh zwei von ihnen einmal scheitern. Setze return True als erste Zeile von origin_allowed, oder nimm and values[0] in trusted aus cors_headers heraus, und führe aus:

python -m pytest -q -k another_origin

Jede Änderung ergibt 1 failed, 2 passed: den Token-Test für die erste, den -Test für die zweite. Mach beide rückgängig. Check my work macht dasselbe mit eigenen Servern, die jeweils einen Fehler machen.

Eine Stelle

Fünf Zeilen in fünf Zweigen fügen jetzt die CORS-Antwort hinzu, und manchen Antworten fehlt ihre. Frag zweimal vom Frontend aus nach /about, beim zweiten Mal mit dem etag der ersten Antwort:

python -c "
import sys
sys.path.insert(0, 'tests')
import helpers, reading_server
state = reading_server.sample_state()
headers = {'Origin': 'https://app.reading.example'}
first = reading_server.handle(helpers.make_request('GET', '/about', headers=headers), state)
headers['If-None-Match'] = first['headers']['etag']
again = reading_server.handle(helpers.make_request('GET', '/about', headers=headers), state)
print(again['status'], list(again['headers']))
"

Es gibt 304 ['etag', 'cache-control'] aus. not_modified() kopiert diese beiden Header und lässt den Rest weg, darunter vary. Eine 304 muss das Vary tragen, das ihre 200 getragen hätte, damit ein Cache die Antwort für einen Origin von der für einen anderen getrennt hält.

Die Abhilfe ist die aus dem Projekt von Kapitel 8: eine Stelle. cors_rule(segments) nennt die Antwort, die ein Pfad bekommt:

PfadRegel
/about"public"
/books und /books/{id}"token"
/me und /tokens"session"
jeder andere Pfad, darunter /books/deleteNone

Dann wendet ein Block in route() sie an, nach den Zeilen, die eine 200 durch eine 304 ersetzen, und vor der Regel no-store:

    rule = cors_rule(segments)
    if rule is not None:
        extra = cors_headers(request, state["trusted_origins"], credentials=rule == "session")
        for name in extra:
            response["headers"][name] = extra[name]

Entferne die fünf Zeilen mit add_headers aus den Lektionen 2 bis 5. Jede Antwort, die diesen Block erreicht, geht jetzt mit derselben Antwort hinaus: die 304, eine 404, eine 405 und die 401 für ein fehlendes Token, die das Frontend jetzt lesen und seinem Benutzer erklären kann. Eine Antwort einer CORS-Route erreicht ihn nie: Die 403 der Prüfung von Origin bei POST /tokens kehrt aus den ersten Zeilen von route() zurück. Sie geht an einen Origin außerhalb des Sets oder an eine Anfrage ohne Header Origin, und keiner von beiden würde ohnehin eine Erlaubnis bekommen. Ein Pfad, der None bekommt, bekommt überhaupt keinen CORS-Header. Führe die Zeile python -c noch einmal aus: Die Liste endet mit 'vary', 'access-control-allow-origin'.

Was CORS nicht getan hat

Führe python pl_sec.py cors aus und lies seine letzte Anfrage:

--- a client that is not a browser: GET /books with the token and no Origin header ---
  your server answered 200
        access-control-allow-origin: (not sent)
        vary: Origin
  books in the answer: 2. This client never looks at a CORS header.

Jeder Header Access-Control- ist an einen Browser gerichtet. Ohne sie zeigt ein Browser einer Seite nichts von einer einfachen Anfrage und sendet nie eine, die einen Preflight braucht. Das ist echt, und es ist das Werk des Browsers. Ein Client, der kein Browser ist, ignoriert all das.

CORS ist also keine Zugriffskontrolle. Es hält nie einen Client auf, der kein Browser ist. Was den Server schützt, ist das, was du vor diesem Kapitel gebaut hast: die Prüfung des Tokens, die Prüfung des Eigentümers, die Prüfung von Origin und das Formular-Token.

Du entfernst jeden CORS-Header vom Server. Welche Anfrage lehnt der Server jetzt ab, die er vorher beantwortet hat?

Meilensteine

  1. Die vier Tests, zwei davon einmal scheitern gesehen. python -m pytest -q endet mit 41 passed.

  2. cors_rule und der Block, ohne die fünf alten Zeilen. Weiterhin 41 passed.

  3. python pl_sec.py cors: the front end read 4 of 4 responses; pages on other origins read 0 of 4.

  4. Zeile F12 von SECURITY.md, in deinen eigenen Worten für die Lese-Hälfte erweitert. Sie nennt die zwei neuen Angriffstests neben den vier, die sie hat, und sagt closed in 9.6.

Ein Schritt wird nicht bewertet. Das mitgelieferte cors_compare.py sendet fünf Anfragen an dein handle() und an Starlettes CORSMiddleware, eine fertige CORS-Antwort aus einem Python-Web-Toolkit, innerhalb eines Programms, und gibt beide Antworten nebeneinander aus. Unterschiede sind zu erwarten und keine Fehler in deinem Code. Wo Starlette nicht installiert ist, sagt die Datei das in einer Zeile und hört auf.

Das Modell verbirgt keine vor einer Seite. Ein Browser zeigt einem Skript auf einem anderen Origin nur Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma und das, was der Server in Access-Control-Expose-Headers aufführt. Deine Tests lesen nur Bodys und Status.

Jedes Versprechen, das dieser Dienst bisher macht, vom mit Secure bis zum Origin einer Seite, setzt HTTPS voraus. Kapitel 10 sieht sich an, worauf diese Voraussetzung beruht.

Klicke dann auf Check my work.

Aufgabe

Schreibe vier Tests, bring die -Antwort des Servers an eine Stelle und schließe einen Befund.

  1. Schreibe in tests/test_attacks.py unter dem Kommentar zu Kapitel 9 vier Tests. Die ersten drei beginnen mit state = steps.keyed_state(), jar = [] und send = ana_logs_in(state, jar) und stellen jede Anfrage einer Seite mit visit.visit(page_origin, wanted, jar, send).

    • test_attack_another_origin_cannot_read_the_list(): Rufe mit headers = {"Authorization": steps.bearer_for(state, "ana", "correct horse")} die steps.OWN + "/books" von steps.APP ab und sichere Status 200 zu. Rufe sie dann von steps.NOTES und steps.OTHER ab, rufe steps.OWN + "/me" mit "credentials": True von beiden ab und sichere jedes Mal zu, dass result["response"] None ist.

    • test_attack_another_origin_cannot_get_a_token(): Sichere mit wanted = {"kind": "fetch", "method": "POST", "url": steps.OWN + "/tokens", "credentials": True} zu, dass die Antwort an steps.APP ein token enthält. Sichere für steps.NOTES zu, dass die Antwort None ist und dass "token" nicht in helpers.json_of(send(result["requests"][0])) ist.

    • test_the_front_end_can_work(): Hol dir von steps.APP aus ein Token von /tokens mit Zugangsdaten, rufe damit /books ab, ändere ein Buch mit PUT und einem -Body und rufe /me mit Zugangsdaten ab. Sichere bei jeder Antwort zu, was die Seite gelesen hat.

    • test_cors_does_not_stop_a_plain_client(): Gib helpers.make_request("GET", "/books", headers=...) mit einem Token und ohne Header Origin an reading_server.handle(...) und sichere Status 200 zu.

  2. Schreibe in reading_server.py die cors_rule(segments): "public" für ["about"], "session" für ["me"] und ["tokens"], "token" für ["books"] und für eine mit zwei Elementen, die mit "books" beginnt und nicht auf "delete" endet, und None für jede andere Liste.

  3. Rufe in route() nach den Zeilen, die eine 200 durch not_modified(response) ersetzen, und vor der Regel no-store die Funktion cors_rule(segments) auf. Wenn die Regel nicht None ist, kopiere jeden Header von cors_headers(request, state["trusted_origins"], credentials=rule == "session") in response["headers"]. Entferne die Aufrufe von add_headers aus den Lektionen 2 bis 5.

  4. Führe python -m pytest -q aus, bis es mit 41 passed endet, und führe dann python pl_sec.py cors aus.

  5. Erweitere in SECURITY.md die Zeile F12 für die Lese-Hälfte. Füge test_attack_another_origin_cannot_read_the_list und test_attack_another_origin_cannot_get_a_token zu den Tests hinzu, die sie nennt, und setze ihren Status auf closed in 9.6.

Klicke dann auf Check my work. Es ruft cors_rule mit fünfzehn Listen von Segmenten auf. Es sendet Anfragen vom des Frontends an handle(): Eine 401, eine 404, drei 405, eine 415, ein HEAD und zwei 304 müssen jeweils die CORS-Antwort tragen, und sechs Pfade ohne Regel dürfen keine tragen. Es reicht die Bytes eines Preflights im Speicher durch http_adapter. Es wiederholt die Prüfungen der Lektionen 2, 4 und 5 für das Modell und den Server. Es führt deine vier Tests, nach Namen ausgewählt, gegen Server aus, die es an die Stelle von reading_server.handle setzt, mit einem eigenen Browsermodell an der Stelle von visit.visit: Jeder Test muss gegen einen Server bestehen, der CORS so beantwortet, wie dieses Kapitel es verlangt, und gegen einen Server mit einem Fehler scheitern, nämlich jedem Origin zu sagen, dass er lesen darf, ein Token auszugeben, egal was der Header Origin sagt, überhaupt keinen CORS-Header zu senden oder jede Anfrage ohne Header Origin abzulehnen. Die beiden Angriffstests müssen außerdem gegen den Server scheitern, der keinen CORS-Header sendet. Kein Test darf eine Verbindung öffnen. Es liest die Zeile F12 von SECURITY.md; ihr Wortlaut wird nicht beurteilt. Nichts aus deinem wird bewertet.