CORS · eindproject
Eindproject: Project: CORS aan beide kanten
De frontend kan nu zijn hele werk doen, in vier verzoeken:
POST /tokensmet credentials, om een token te krijgen voor devan ana. GET /booksmet het token, na een preflight.PUT /books/1met het token en een-body, na een preflight. GET /memet credentials, om te tonen wie er is ingelogd.
Nog niets zegt wat een pagina op een andere
Vier tests
Alle vier komen in tests/test_attacks.py. Drie ervan beginnen op dezelfde manier, met ana ingelogd via de eigen pagina van de leeslijst:
state = steps.keyed_state()
jar = []
send = ana_logs_in(state, jar)
Daarna is het verzoek van een pagina visit.visit(page_origin, wanted, jar, send), en result["response"] is wat het script van de pagina mag lezen, of None.
test_the_front_end_can_work speelt de vier verzoeken hierboven vanaf steps.APP en assert elke keer op wat de pagina las: een token, een status 200, twee boeken, de gebruiker ana.
test_attack_another_origin_cannot_read_the_list haalt /books op met het token van ana, en /me met credentials, vanaf steps.NOTES en vanaf steps.OTHER. Elk antwoord moet None zijn. Eerst assert hij dat dezelfde fetch van /books werkt vanaf steps.APP, zodat de test niet kan slagen tegen een server waarop niemand kan lezen.
test_attack_another_origin_cannot_get_a_token stuurt de POST /tokens van les 5 vanaf de twee andere origins. De pagina leest niets, en dat alleen bewijst te weinig: een server zou een token kunnen maken en het alleen niet tonen. Een pagina ziet nooit een antwoord dat ze niet mag lezen. Een test wel:
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 gebruikt helemaal geen browser. Hij geeft GET /books met een token en zonder header Origin rechtstreeks aan reading_server.handle en assert 200 en twee boeken.
De tests slagen zodra je ze schrijft, omdat les 5 de server correct achterliet. Zie er twee één keer mislukken. Zet return True als eerste regel van origin_allowed, of haal and values[0] in trusted uit cors_headers, en voer dit uit:
python -m pytest -q -k another_origin
Elke wijziging geeft 1 failed, 2 passed: de tokentest bij de eerste, de lijsttest bij de tweede. Maak beide ongedaan. Check my work doet hetzelfde met eigen servers die elk één fout maken.
Eén plek
Vijf regels in vijf takken voegen nu het CORS-antwoord toe, en sommige antwoorden missen het hunne. Vraag /about twee keer op vanaf de frontend, de tweede keer met de etag van het eerste antwoord:
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']))
"
Dat drukt 304 ['etag', 'cache-control'] af. not_modified() kopieert die twee headers en laat de rest vallen, waaronder vary. Een 304 moet de Vary dragen die zijn 200 gedragen zou hebben, zodat een cache het antwoord voor de ene origin gescheiden houdt van dat voor een andere.
De oplossing is die van het project van hoofdstuk 8: één plek. cors_rule(segments) noemt het antwoord dat een pad krijgt:
| Pad | Regel |
|---|---|
/about | "public" |
/books en /books/{id} | "token" |
/me en /tokens | "session" |
elk ander pad, waaronder /books/delete | None |
Daarna past één blok in route() hem toe, na de regels die een 200 vervangen door een 304 en vóór de regel voor 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]
Verwijder de vijf regels met add_headers uit les 2 tot en met 5. Elk antwoord dat dit blok bereikt, vertrekt nu met hetzelfde antwoord: de 304, een 404, een 405, en de 401 voor een ontbrekend token, die de frontend nu kan lezen en aan zijn gebruiker kan uitleggen. Eén antwoord van een CORS-route bereikt het nooit: de 403 van de controle van Origin op POST /tokens keert terug vanuit de eerste regels van route(). Die gaat naar een origin buiten de set, of naar een verzoek zonder header Origin, en geen van beide zou toch een toestemming krijgen. Een pad dat None krijgt, krijgt helemaal geen CORS-header. Voer de regel python -c opnieuw uit: de 'vary', 'access-control-allow-origin'.
Wat CORS niet deed
Voer python pl_sec.py cors uit en lees het laatste verzoek:
--- 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.
Elke header Access-Control- is gericht aan een browser. Zonder die headers laat een browser een pagina niets zien van een eenvoudig verzoek en verstuurt hij nooit een verzoek dat een preflight nodig heeft. Dat is echt, en het is het werk van de browser. Een client die geen browser is, negeert dat allemaal.
CORS is dus geen toegangscontrole. Het houdt nooit een client tegen die geen browser is. Wat de server beschermt, is wat je vóór dit hoofdstuk bouwde: de controle van het token, de controle van de eigenaar, de controle van Origin en het formuliertoken.
Je verwijdert elke CORS-header van de server. Welk verzoek weigert de server nu dat hij eerder beantwoordde?
Mijlpalen
De vier tests, waarvan je er twee één keer hebt zien mislukken.
python -m pytest -qeindigt met41 passed.cors_ruleen het blok, met de vijf oude regels weg. Nog steeds41 passed.python pl_sec.py cors:the front end read 4 of 4 responses; pages on other origins read 0 of 4.Rij F12 van
SECURITY.md, in je eigen woorden uitgebreid voor de leeshelft. Hij noemt de twee nieuwe aanvalstests naast de vier die hij al heeft, en zegtclosed in 9.6.
Eén stap wordt niet beoordeeld. De meegeleverde cors_compare.py stuurt vijf verzoeken naar je handle() en naar CORSMiddleware van Starlette, een kant-en-klaar CORS-antwoord uit een Python-webtoolkit, binnen één programma, en drukt beide antwoorden naast elkaar af. Verschillen zijn te verwachten en zijn geen fouten in je code. Waar Starlette niet geïnstalleerd is, zegt het bestand dat in één regel en stopt het.
Het model verbergt geen Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma zien, en wat de server opsomt in Access-Control-Expose-Headers. Je tests lezen alleen bodies en statussen.
Elke belofte die deze service tot nu toe doet, van de Secure-
Druk daarna op Check my work.
Opdracht
Schrijf vier tests, zet het
Schrijf in
tests/test_attacks.py, onder het commentaar voor hoofdstuk 9, vier tests. De eerste drie beginnen metstate = steps.keyed_state(),jar = []ensend = ana_logs_in(state, jar), en doen elk verzoek van een pagina metvisit.visit(page_origin, wanted, jar, send).test_attack_another_origin_cannot_read_the_list(): haal metheaders = {"Authorization": steps.bearer_for(state, "ana", "correct horse")}steps.OWN + "/books"op vanafsteps.APPen assert status 200. Haal het daarna op vanafsteps.NOTESensteps.OTHER, haal vanaf beidesteps.OWN + "/me"op met"credentials": True, en assert datresult["response"]elke keerNoneis.test_attack_another_origin_cannot_get_a_token(): assert metwanted = {"kind": "fetch", "method": "POST", "url": steps.OWN + "/tokens", "credentials": True}dat het antwoord aansteps.APPeentokenbevat. Assert voorsteps.NOTESdat het antwoordNoneis en dat"token"niet inhelpers.json_of(send(result["requests"][0]))zit.test_the_front_end_can_work(): haal vanafsteps.APPeen token op bij/tokensmet credentials, haal/booksermee op, wijzig een boek metPUTen een-body, en haal /meop met credentials. Assert op elk antwoord dat de pagina las.test_cors_does_not_stop_a_plain_client(): geefhelpers.make_request("GET", "/books", headers=...)met een token en zonder headerOriginaanreading_server.handle(...), en assert status 200.
Schrijf in
reading_server.pycors_rule(segments):"public"voor["about"],"session"voor["me"]en["tokens"],"token"voor["books"]en voor eenvan twee die begint met "books"en niet eindigt op"delete", enNonevoor elke andere lijst.Roep in
route(), na de regels die een 200 vervangen doornot_modified(response)en vóór de regel voorno-store,cors_rule(segments)aan. Als de regel nietNoneis, kopieer dan elke header vancors_headers(request, state["trusted_origins"], credentials=rule == "session")naarresponse["headers"]. Verwijder de aanroepen vanadd_headersuit les 2 tot en met 5.Voer
python -m pytest -quit tot het eindigt met41 passed, en voer daarnapython pl_sec.py corsuit.Breid in
SECURITY.mdrij F12 uit voor de leeshelft. Voegtest_attack_another_origin_cannot_read_the_listentest_attack_another_origin_cannot_get_a_tokentoe aan de tests die hij noemt, en zet de status opclosed in 9.6.
Druk daarna op Check my work. Dat roept cors_rule aan met vijftien lijsten met segmenten. Het stuurt verzoeken vanaf de handle(): een 401, een 404, drie 405’s, een 415, een HEAD en twee 304’s moeten elk het CORS-antwoord dragen, en zes paden zonder regel mogen er geen dragen. Het geeft de bytes van een preflight in het geheugen door http_adapter. Het herhaalt de controles van les 2, 4 en 5 voor het model en de server. Het voert je vier tests, bij naam geselecteerd, uit tegen servers die het in de plaats van reading_server.handle zet, met een eigen browsermodel in de plaats van visit.visit: elke test moet slagen tegen een server die CORS beantwoordt zoals dit hoofdstuk vraagt, en mislukken tegen een server met één fout, namelijk elke origin vertellen dat hij mag lezen, een token uitdelen wat de header Origin ook zegt, helemaal geen CORS-header sturen, of elk verzoek zonder header Origin weigeren. De twee aanvalstests moeten ook mislukken tegen de server die geen CORS-header stuurt. Geen enkele test mag een verbinding openen. Het leest rij F12 van SECURITY.md; de formulering wordt niet beoordeeld. Niets uit je