0%

CORS · oefening

Antwoord vanuit een exacte allowlist

Les 1 volgde de uitwisseling op papier. Nu schrijf je beide helften ervan. De helft van de server is cors_headers in reading_server.py, die beslist wat het antwoord zegt. De helft van de browser is may_read in mini_browser.py, die beslist wat de pagina krijgt. Check my work test je server tegen een eigen browsermodel, en je model tegen eigen antwoorden, zodat de twee helften het niet over een fout eens kunnen worden.

De helft van de server

cors_headers(request, trusted) geeft een terug met headers die aan een antwoord worden toegevoegd. trusted is een set . De state heeft voor dit hoofdstuk zo’n set, state["cors_origins"], en die bevat één origin: https://app.reading.example.

De beslissing is dezelfde die origin_allowed in hoofdstuk 8 nam. Lees de van de header Origin van het verzoek. Als er precies één is, en de set bevat die, noemt het antwoord diezelfde origin:

access-control-allow-origin: https://app.reading.example

In alle andere gevallen heeft het antwoord zo’n header niet. De server weigert niets: hij stuurt zijn gewone antwoord en laat de toestemming weg.

De vergelijking is values[0] in trusted, de hele tekst tegen de leden van een set. Drie kortere wegen lijken minder werk, en elk geeft iets weg:

  • De header Origin in het antwoord kopiëren. Elke pagina die het vraagt, krijgt te horen dat zij, bij naam, mag lezen. De header staat dan elke origin toe die er bestaat.

  • Een test met startswith of in op de tekst. https://app.reading.example.other.example begint met de vertrouwde origin, en hij is van wie other.example bezit. Hoofdstuk 8 liet deze lookalikes zien.

  • null in de set zetten. Een browser schrijft de origin van sommige pagina’s, bijvoorbeeld een pagina in een sandbox, als de tekst null. Een set die "null" bevat, staat elke zulke pagina toe, wie hem ook gemaakt heeft.

Een server kopieert de Origin van het verzoek op elk antwoord naar Access-Control-Allow-Origin. Welke pagina's mogen zijn antwoorden lezen?

Vary: Origin

Tot nu toe gaf /about iedereen hetzelfde antwoord. Vanaf deze les hangen de headers ervan af van een header van het verzoek.

Dat is van belang voor een cache. In with Python bewaarde een cache een antwoord onder zijn . Een cache die tussen veel lezers en je server zit en /about zo bewaart, zou het eerste antwoord houden dat hij zag en dat aan iedereen geven. Als dat eerste antwoord naar een pagina op een andere origin ging, zit er geen toestemming in, en de frontend krijgt het en mag niet lezen. Met twee toegestane origins zou elk een antwoord kunnen krijgen dat de andere noemt. De cache geeft niemand een leesrecht dat hij niet hoort te hebben: een pagina die het antwoord voor de frontend krijgt, zakt nog steeds voor de controle van de browser, omdat de header een andere origin noemt. Wat er stukgaat, zijn de lezers die je wel toestond.

Nog één voorkomt dat:

vary: Origin

Hij vertelt een cache dat het antwoord afhangt van de header Origin van het verzoek, zodat antwoorden voor verschillende origins gescheiden moeten blijven. De weigering hangt evenveel van die header af als de toestemming. Daarom zet cors_headers vary als eerste in zijn dict en geeft hij die in elk geval terug, ook voor een verzoek zonder header Origin.

Niets in deze cursus laat Vary aan het werk zien, omdat het model geen cache heeft. De reden die hier staat, is het hele . Een server die Vary al om een andere reden zet, zou de twee waarden moeten samenvoegen. Deze heeft geen andere reden.

Waarom draagt het antwoord Vary: Origin, ook als de origin wordt geweigerd?

Wanneer een ster prima is

Access-Control-Allow-Origin: * staat elke origin toe. Dat is een redelijk antwoord voor een antwoord dat voor iedereen hetzelfde is en geen login nodig heeft: iedereen kan het toch al ophalen. Het is nooit goed voor een antwoord achter een login. Browsers weigeren een ster zodra de pagina vroeg om mee te sturen met haar verzoek, en les 5 komt op die regel terug.

/about zou in aanmerking komen voor een ster. Deze service noemt ook daar de origin, zodat hij één regel te controleren heeft en geen twee.

Wanneer is Access-Control-Allow-Origin: * aanvaardbaar?

De helft van de browser

may_read(response, page_origin, *, credentials) is de controle die je in les 1 uitvoerde, als een die True of False teruggeeft. De meegeleverde response_header(response, name) geeft je één header van een antwoord als tekst, of None.

  • Geen access-control-allow-origin: nee.

  • De waarde *, of precies page_origin: ja.

  • Elke andere waarde: nee. Dat geldt ook voor een waarde met een slash aan het eind, twee origins in één waarde, en dezelfde letters met andere hoofdletters.

credentials zegt of de pagina vroeg om cookies mee te sturen met haar verzoek. Les 5 schrijft de regel voor dat geval. Begin de functie tot dan met if credentials: return False. Het model weigert wat het nog niet begrijpt, zoals load_keys deed in hoofdstuk 5.

Je opdracht

Schrijf in reading_server.py de functie cors_headers. Gebruik hem daarna in route(). Voeg in de tak "/about", in het geval voor GET en HEAD, één regel toe nadat het antwoord is gemaakt:

            response = add_headers(response, cors_headers(request, state["cors_origins"]))

De meegeleverde add_headers kopieert een dict met headers naar een antwoord en geeft het antwoord terug. Vraag je functie wat hij antwoordt:

python -c "import reading_server; print(reading_server.cors_headers({'headers': {'origin': ['https://app.reading.example']}}, {'https://app.reading.example'}))"

Dat drukt {'vary': 'Origin', 'access-control-allow-origin': 'https://app.reading.example'} af. Met elke andere origin in het verzoek drukt het {'vary': 'Origin'} af.

Schrijf in mini_browser.py de functie may_read.

Schrijf daarna één test, test_a_page_without_permission_cannot_read, in tests/test_browser.py. Die stuurt GET /about met de header Origin: https://app.reading.example rechtstreeks naar reading_server.handle, en geeft het antwoord twee keer aan mini_browser.may_read: voor een pagina op steps.APP, die het mag lezen, en voor een pagina op steps.OTHER, die dat niet mag. visit() kan dit verzoek nog niet voor je doen. Daarvoor is een functie nodig die les 5 schrijft.

python -m pytest -q
python mini_browser.py

De eerste eindigt met 36 passed. In de tweede tabel van python mini_browser.py zegt de eerste kolom “may read” nu True voor de frontend en False voor de andere twee pagina’s. De laatste kolom zegt drie keer False, en blijft zo tot les 5.

Druk daarna op Check my work.

Opdracht

Schrijf het -antwoord van de server en de controle daarvan door de browser, en één test.

In reading_server.py:

  • cors_headers(request, trusted) geeft een met headers terug. Die bevat altijd "vary": "Origin". Als het verzoek precies één origin heeft en de set trusted die waarde bevat, bevat hij ook "access-control-allow-origin" met diezelfde waarde. Hij antwoordt nooit *, en hij vergelijkt de hele waarde met de leden van de set.

  • In de tak "/about" van route(), in het geval voor GET en HEAD: response = add_headers(response, cors_headers(request, state["cors_origins"])).

In mini_browser.py:

  • may_read(response, page_origin, *, credentials) geeft False terug als credentials waar is. Anders leest hij response_header(response, "access-control-allow-origin") en geeft hij True terug als de waarde "*" is of gelijk is aan page_origin, en False voor een ontbrekende header en voor elke andere waarde.

Schrijf in tests/test_browser.py, onder het commentaar voor hoofdstuk 9, test_a_page_without_permission_cannot_read. Bouw helpers.make_request("GET", "/about", headers={"Origin": steps.APP}), geef dat aan reading_server.handle(request, state) met state = reading_server.sample_state(), en assert dat mini_browser.may_read(response, steps.APP, credentials=False) True is en dat mini_browser.may_read(response, steps.OTHER, credentials=False) False is.

Voer python -m pytest -q en python mini_browser.py uit.

Druk daarna op Check my work. Dat roept cors_headers aan met verzoeken en een set van twee die je niet hebt gezien: beide vertrouwde origins, een andere site, een naburige host, hosts die alleen op een vertrouwde lijken, een andere , de http-vorm, de tekst null, twee headers Origin, en geen enkele. Het stuurt GET en HEAD /about naar handle() vanaf drie origins en zonder origin, en laat een eigen browsermodel de antwoorden beoordelen. Het roept may_read aan met eigen antwoorden: geen header, de exacte origin, een ster, een toegestane 404, en acht waarden die alleen goed lijken, en twee keer met credentials=True. Als laatste voert het je test één keer bij naam uit: die moet slagen, en hij moet reading_server.handle en mini_browser.may_read via hun aanroepen. Niets uit je wordt beoordeeld.