0%

CORS · eindproject

Eindproject: Project: CORS aan beide kanten

De frontend kan nu zijn hele werk doen, in vier verzoeken:

  1. POST /tokens met credentials, om een token te krijgen voor de van ana.

  2. GET /books met het token, na een preflight.

  3. PUT /books/1 met het token en een -body, na een preflight.

  4. GET /me met credentials, om te tonen wie er is ingelogd.

Nog niets zegt wat een pagina op een andere niet kan. Dit project schrijft die tests, en het verplaatst het -antwoord van de server naar één plek.

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:

PadRegel
/about"public"
/books en /books/{id}"token"
/me en /tokens"session"
elk ander pad, waaronder /books/deleteNone

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 eindigt met '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

  1. De vier tests, waarvan je er twee één keer hebt zien mislukken. python -m pytest -q eindigt met 41 passed.

  2. cors_rule en het blok, met de vijf oude regels weg. Nog steeds 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. 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 zegt closed 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 voor een pagina. Een browser laat een script op een andere origin alleen 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- tot de origin van een pagina, gaat uit van HTTPS. Hoofdstuk 10 kijkt waar die aanname op .

Druk daarna op Check my work.

Opdracht

Schrijf vier tests, zet het -antwoord van de server op één plek, en sluit een bevinding.

  1. Schrijf in tests/test_attacks.py, onder het commentaar voor hoofdstuk 9, vier tests. De eerste drie beginnen met state = steps.keyed_state(), jar = [] en send = ana_logs_in(state, jar), en doen elk verzoek van een pagina met visit.visit(page_origin, wanted, jar, send).

    • test_attack_another_origin_cannot_read_the_list(): haal met headers = {"Authorization": steps.bearer_for(state, "ana", "correct horse")} steps.OWN + "/books" op vanaf steps.APP en assert status 200. Haal het daarna op vanaf steps.NOTES en steps.OTHER, haal vanaf beide steps.OWN + "/me" op met "credentials": True, en assert dat result["response"] elke keer None is.

    • test_attack_another_origin_cannot_get_a_token(): assert met wanted = {"kind": "fetch", "method": "POST", "url": steps.OWN + "/tokens", "credentials": True} dat het antwoord aan steps.APP een token bevat. Assert voor steps.NOTES dat het antwoord None is en dat "token" niet in helpers.json_of(send(result["requests"][0])) zit.

    • test_the_front_end_can_work(): haal vanaf steps.APP een token op bij /tokens met credentials, haal /books ermee op, wijzig een boek met PUT en een -body, en haal /me op met credentials. Assert op elk antwoord dat de pagina las.

    • test_cors_does_not_stop_a_plain_client(): geef helpers.make_request("GET", "/books", headers=...) met een token en zonder header Origin aan reading_server.handle(...), en assert status 200.

  2. Schrijf in reading_server.py cors_rule(segments): "public" voor ["about"], "session" voor ["me"] en ["tokens"], "token" voor ["books"] en voor een van twee die begint met "books" en niet eindigt op "delete", en None voor elke andere lijst.

  3. Roep in route(), na de regels die een 200 vervangen door not_modified(response) en vóór de regel voor no-store, cors_rule(segments) aan. Als de regel niet None is, kopieer dan elke header van cors_headers(request, state["trusted_origins"], credentials=rule == "session") naar response["headers"]. Verwijder de aanroepen van add_headers uit les 2 tot en met 5.

  4. Voer python -m pytest -q uit tot het eindigt met 41 passed, en voer daarna python pl_sec.py cors uit.

  5. Breid in SECURITY.md rij F12 uit voor de leeshelft. Voeg test_attack_another_origin_cannot_read_the_list en test_attack_another_origin_cannot_get_a_token toe aan de tests die hij noemt, en zet de status op closed in 9.6.

Druk daarna op Check my work. Dat roept cors_rule aan met vijftien lijsten met segmenten. Het stuurt verzoeken vanaf de van de frontend naar 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 wordt beoordeeld.