CORS · Abschlussprojekt
Abschlussprojekt: Projekt: CORS bei Server und Browser
Das Frontend kann jetzt seine ganze Arbeit erledigen, in vier Anfragen:
POST /tokensmit Zugangsdaten, um ein Token für anaszu bekommen. GET /booksmit dem Token, nach einem Preflight.PUT /books/1mit dem Token und einem-Body, nach einem Preflight. GET /memit Zugangsdaten, um anzuzeigen, wer angemeldet ist.
Noch nichts sagt, was eine Seite auf einem anderen
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
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:
| Pfad | Regel |
|---|---|
/about | "public" |
/books und /books/{id} | "token" |
/me und /tokens | "session" |
jeder andere Pfad, darunter /books/delete | None |
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
Die vier Tests, zwei davon einmal scheitern gesehen.
python -m pytest -qendet mit41 passed.cors_ruleund der Block, ohne die fünf alten Zeilen. Weiterhin41 passed.python pl_sec.py cors:the front end read 4 of 4 responses; pages on other origins read 0 of 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 sagtclosed 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 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 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
Schreibe in
tests/test_attacks.pyunter dem Kommentar zu Kapitel 9 vier Tests. Die ersten drei beginnen mitstate = steps.keyed_state(),jar = []undsend = ana_logs_in(state, jar)und stellen jede Anfrage einer Seite mitvisit.visit(page_origin, wanted, jar, send).test_attack_another_origin_cannot_read_the_list(): Rufe mitheaders = {"Authorization": steps.bearer_for(state, "ana", "correct horse")}diesteps.OWN + "/books"vonsteps.APPab und sichere Status 200 zu. Rufe sie dann vonsteps.NOTESundsteps.OTHERab, rufesteps.OWN + "/me"mit"credentials": Truevon beiden ab und sichere jedes Mal zu, dassresult["response"]Noneist.test_attack_another_origin_cannot_get_a_token(): Sichere mitwanted = {"kind": "fetch", "method": "POST", "url": steps.OWN + "/tokens", "credentials": True}zu, dass die Antwort ansteps.APPeintokenenthält. Sichere fürsteps.NOTESzu, dass die AntwortNoneist und dass"token"nicht inhelpers.json_of(send(result["requests"][0]))ist.test_the_front_end_can_work(): Hol dir vonsteps.APPaus ein Token von/tokensmit Zugangsdaten, rufe damit/booksab, ändere ein Buch mitPUTund einem-Body und rufe /memit Zugangsdaten ab. Sichere bei jeder Antwort zu, was die Seite gelesen hat.test_cors_does_not_stop_a_plain_client(): Gibhelpers.make_request("GET", "/books", headers=...)mit einem Token und ohne HeaderOriginanreading_server.handle(...)und sichere Status 200 zu.
Schreibe in
reading_server.pydiecors_rule(segments):"public"für["about"],"session"für["me"]und["tokens"],"token"für["books"]und für einemit zwei Elementen, die mit "books"beginnt und nicht auf"delete"endet, undNonefür jede andere Liste.Rufe in
route()nach den Zeilen, die eine 200 durchnot_modified(response)ersetzen, und vor der Regelno-storedie Funktioncors_rule(segments)auf. Wenn die Regel nichtNoneist, kopiere jeden Header voncors_headers(request, state["trusted_origins"], credentials=rule == "session")inresponse["headers"]. Entferne die Aufrufe vonadd_headersaus den Lektionen 2 bis 5.Führe
python -m pytest -qaus, bis es mit41 passedendet, und führe dannpython pl_sec.py corsaus.Erweitere in
SECURITY.mddie Zeile F12 für die Lese-Hälfte. Fügetest_attack_another_origin_cannot_read_the_listundtest_attack_another_origin_cannot_get_a_tokenzu den Tests hinzu, die sie nennt, und setze ihren Status aufclosed in 9.6.
Klicke dann auf Check my work. Es ruft cors_rule mit fünfzehn Listen von Segmenten auf. Es sendet Anfragen vom 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