0%

CORS · oefening

Cookies tussen origins

De frontend kan de boekenlijst lezen zodra hij een token heeft. Tot nu toe gaf een test hem er een. Een echte pagina moet haar token ergens vandaan halen.

POST /tokens geeft een token aan een ingelogde , en de sessie leeft in een van reading.example. De pagina van de frontend moet dus een verzoek naar een andere sturen met de cookie van ana erop, en het antwoord lezen. Voor beide delen is een regel nodig die het model nog niet heeft.

Een fetch laat cookies thuis, tenzij erom gevraagd wordt

In hoofdstuk 8 keek elk verzoek naar de cookiejar. Dat klopte voor die verzoeken: een formulier, een link en een gewone afbeelding doen dat altijd. De fetch van een script doet dat alleen voor de eigen origin. Voor een andere origin laat hij elke cookie thuis, tenzij de pagina om credentials vraagt. In het model is dat "credentials": True in de wanted.

attaches_cookies(kind, url, page_origin, *, credentials) legt die regel vast: True voor elk soort behalve "fetch", True voor een fetch naar de eigen origin van de pagina, en anders wat credentials zegt. Hij beantwoordt of er überhaupt naar cookies wordt gekeken. Welke cookies er dan meegaan, blijft de zaak van cookies_to_send.

Een script op https://app.reading.example haalt https://reading.example/me op met fetch en vraagt om niets bijzonders. Gaat de sessiecookie mee?

Een strengere toestemming

Een antwoord op een verzoek dat een cookie meedroeg, kan iets bevatten wat alleen de ingelogde lezer zou mogen zien. Daarom vraagt een browser meer van de server voordat een pagina het mag lezen:

  • Access-Control-Allow-Origin moet de exacte origin van de pagina noemen. Een * wordt niet geaccepteerd.

  • Het antwoord moet ook Access-Control-Allow-Credentials: true zeggen, precies zo gespeld.

may_read krijgt deze regel in plaats van de return False van les 2. Hetzelfde geldt voor een preflight die vóór een verzoek met credentials komt. preflight_allows krijgt een keyword-only , credentials=False. Hij geeft de door aan may_read, en als die waar is, telt een * in de met of in de lijst met headers niet mee.

Op de server krijgt cors_headers hetzelfde keyword. Met credentials=True krijgt een origin die is toegestaan ook access-control-allow-credentials: true. Een origin die niet is toegestaan, krijgt geen van beide headers.

Een antwoord op een fetch met credentials draagt Access-Control-Allow-Origin: * en Access-Control-Allow-Credentials: true. Mag de pagina het lezen?

Volg het token

Hier is de POST /tokens van de frontend, één beslissing tegelijk.

  1. Het verzoek heeft geen body en geen eigen header van de pagina. Het is een eenvoudig verzoek, dus er wordt geen preflight verstuurd.

  2. De pagina vroeg om credentials, dus attaches_cookies zegt ja. De sessiecookie is SameSite=Lax, en de twee hosts zijn één site, dus cookies_to_send laat hem gaan.

  3. De browser voegt Origin: https://app.reading.example toe. Je server controleert die header op deze route sinds hoofdstuk 8, en vandaag zou hij weigeren. De origin van de frontend moet bij state["trusted_origins"] komen.

  4. De server maakt een token. cors_headers noemt, met credentials=True, de origin van de frontend en staat credentials toe.

  5. may_read vindt de exacte origin en true, en de pagina leest haar token.

Een pagina op notes.reading.example kan dezelfde POST sturen. Ze staat op dezelfde site en mag ook om credentials vragen, dus de cookie van ana gaat mee. De controle van Origin weigert het verzoek met 403 voordat er een token wordt gemaakt. En de pagina had een antwoord toch niet kunnen lezen, omdat geen enkel antwoord haar origin noemt.

Een pagina op notes.reading.example doet dezelfde POST /tokens met credentials. Wat houdt haar tegen om een token te krijgen?

Eén set

Na stap 3 zou je state twee keer dezelfde beslissing bevatten: cors_origins voor lezen, trusted_origins voor de controle van Origin. Twee sets die het eens moeten zijn, zijn het op een dag oneens. Verwijder cors_origins uit sample_state(), zet de origin van de frontend in trusted_origins, en geef die ene set door aan elke aanroep van cors_headers.

De eigen origin van de service staat nu in de set die gebruikt. Dat verandert niets, omdat een pagina nooit CORS nodig heeft om haar eigen origin te lezen.

De frontend heeft er ook iets bij gekregen. Elke POST die de controle van Origin bewaakt, accepteert hem nu, dus zijn pagina kan POST /logout sturen en ana uitloggen. Een origin die je met credentials laat lezen, is een origin die je met de sessie vertrouwt.

Vertrouwen heeft een grens. De frontend kan het verwijderformulier nog steeds niet posten: POST /books/delete vraagt om het token van de eigen pagina van de leeslijst, en de pagina van de frontend heeft dat niet.

Maar twee routes antwoorden met credentials: GET /me en POST /tokens. De boekroutes hebben er geen nodig. Een bearer-token dat een script in een header zet, is geen credential in de zin van de browser, en daar wordt geen cookie gelezen.

Lees de rest van visit.py

Open visit.py en zoek het blok dat met hoofdstuk 9 is gemarkeerd. De negen regels ervan roepen vier aan, en die heb je allemaal geschreven. Vanaf nu draagt visit() een fetch naar een andere origin van begin tot eind.

Je opdracht

In mini_browser.py: schrijf attaches_cookies, en breid may_read en preflight_allows uit.

In reading_server.py: voeg het keyword toe aan cors_headers, maak de wijziging in sample_state(), en geef state["trusted_origins"] door in de drie aanroepen die je hebt. Voeg daarna één regel toe aan de tak "/me", in het geval voor GET en HEAD, en dezelfde regel aan de tak "/tokens", in het geval voor POST:

            response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True))

Kijk daarna naar de hele uitwisseling op :

python -m pytest -q
python pl_sec.py cors

De tests eindigen met 37 passed. Het scenario speelt de frontend, daarna de buurman, daarna een andere site. Dit is het eerste verzoek van de frontend en het laatste van de buurman:

--- the front end, https://app.reading.example: POST /tokens, and it asks for credentials ---
  sent: POST /tokens   cookie: yes   token: no   origin: https://app.reading.example
  your server answered 200
        access-control-allow-origin: https://app.reading.example
        access-control-allow-credentials: true
        vary: Origin
  the page may read the response: yes
  the page now holds a token (not shown)
  sent: GET /me   cookie: yes   token: no   origin: https://notes.reading.example
  your server answered 200
        vary: Origin
  the page may read the response: no (the response did not give this origin permission to read it)

De laatste regel van het resultaat zegt the front end read 4 of 4 responses; pages on other origins read 0 of 4; the request without an Origin header was answered 200.

Het model kent alleen cookies. In de standaard vallen onder credentials ook een login die de browser onthoudt voor -authenticatie, en clientcertificaten. En de frontend heeft geluk dat hij een site deelt met de service. Een frontend op een andere site zou de sessiecookie SameSite=None nodig hebben, en dat is een grotere beslissing dan deze les neemt. Zelfs dan kan een browser de cookie achterhouden. Safari blokkeert cookies die naar een derde partij gaan, Firefox houdt voor elke site een aparte cookiejar bij, en Chrome stuurt ze mee, tenzij de gebruiker dat heeft uitgezet of in een incognitovenster zit.

Druk daarna op Check my work.

Opdracht

Leer het model wanneer een fetch draagt en wat een pagina dan mag lezen, en laat de server dat op twee routes toestaan.

In mini_browser.py:

  • attaches_cookies(kind, url, page_origin, *, credentials) geeft True terug als kind niet "fetch" is, True als same_origin(url, page_origin), en anders de van credentials.

  • may_read(response, page_origin, *, credentials): zonder credentials zoals in les 2. Met credentials geeft hij alleen True terug als access-control-allow-origin gelijk is aan page_origin en access-control-allow-credentials precies "true" is. Een * geeft dan geen toestemming.

  • preflight_allows(response, page_origin, method, headers, *, credentials=False) geeft credentials door aan may_read. Als credentials waar is, telt een * in access-control-allow-methods of in access-control-allow-headers niet mee.

In reading_server.py:

  • cors_headers(request, trusted, *, credentials=False): voor een die is toegestaan, en alleen dan, voegt credentials=True "access-control-allow-credentials": "true" toe.

  • In sample_state() bevat "trusted_origins" "https://reading.example" en "https://app.reading.example", en "cors_origins" is weg. De drie aanroepen van cors_headers uit les 2 en 4 geven state["trusted_origins"] door.

  • In de tak "/me", in het geval voor GET en HEAD, en in de tak "/tokens", in het geval voor POST: response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True)). Geen andere route staat credentials toe.

Voer python -m pytest -q, python mini_browser.py en python pl_sec.py cors uit.

Druk daarna op Check my work. Dat roept attaches_cookies aan met tien verzoeken die je niet hebt gezien: een formulier, een link, een afbeelding, en fetches naar de eigen origin van de pagina, naar een andere origin en naar een andere , met en zonder credentials. Het roept may_read en preflight_allows aan met eigen antwoorden: met en zonder de credentials-header, met een ster op elke plek, en met andere spellingen van true. Het roept cors_headers aan met credentials=True voor origins binnen en buiten de set. Daarna gebruikt het een eigen state zonder cors_origins, logt het een gebruiker in, en stuurt het verzoeken naar handle(): /me en /tokens vanaf de origin van de frontend en vanaf twee andere, en negen andere routes, waarvan geen enkele credentials mag toestaan of een ster mag antwoorden. Een eigen browsermodel speelt de frontend, een buurman en een andere site tegen je server. Het leest ook sample_state()["trusted_origins"]. Niets uit je wordt beoordeeld.