0%

Eindproject: maak de leeslijst veilig · eindproject

Eindproject: Dicht de cross-origin gaten

Negen bevindingen zijn gedicht. De twee rode tests die over zijn, gaan over verzoeken die een pagina op een andere site veroorzaakt, en daarvoor heb je een hulpmiddel: de modelbrowser die je in hoofdstuk 8 en 9 schreef. Het contract zegt dat een van zijn regels is verzwakt. Dus het hulpmiddel komt eerst.

De reden is de richting van de fout. Een model dat in de strenge richting fout zit, zou verzoeken weigeren die een browser wel stuurt, en dat zou je meteen merken, omdat de eigen pagina van de service dan niet meer werkt. Een model dat in de gulle richting fout zit, is stiller. Het laat aanvallen aankomen die een echte browser had tegengehouden, en het stuurt je op pad naar verdedigingen voor verzoeken die niet kunnen gebeuren, terwijl het je niets waars vertelt over de verzoeken die wel kunnen gebeuren.

Vind de regel

Het model heeft zijn eigen tests. Voer ze uit:

python -m pytest tests/test_browser.py -q

Vier van de zes falen, en de samenvatting aan het eind noemt ze:

  • test_lax_cookie_stays_home_on_a_cross_site_post

  • test_samesite_stops_a_form_from_another_site

  • test_a_page_without_permission_cannot_read

  • test_a_delete_is_asked_about_first

De laatste twee zijn je tests uit hoofdstuk 9, en ze falen op het antwoord van de server, niet op het model: https://other.example krijgt te horen dat het GET /about mag lezen en een DELETE mag sturen. Dat is bevinding 11, en die wacht tot het eind van deze les. De eerste twee gaan over het model.

Lees de eerste. Hij bouwt één met SameSite=Lax, vraagt mini_browser.cookies_to_send() wat er meegaat met een top-level POST vanaf https://other.example, en verwacht niets. Hij krijgt de cookie. De tweede test is dezelfde fout, gezien via visit.visit(): een formulier op een andere site komt aan met Ana’s sessiecookie.

cookies_to_send() vraagt één naar SameSite, en daar moet je kijken. De regel uit hoofdstuk 8: een Lax-cookie gaat alleen mee met een cross-site verzoek als het verzoek top-level is en de GET of HEAD is. Een link die iemand volgt, neemt hem mee. Een formulier dat post, niet.

Wat laat het model met de verzwakte regel zien als een pagina op https://other.example het verwijderformulier post?

Herstel de regel en voer de zes tests opnieuw uit voordat je verdergaat. De twee modeltests slagen; de twee tests uit hoofdstuk 9 blijven falen tot bevinding 11 is gedicht. Pas op dat je niet doorschiet: een Lax-cookie gaat nog steeds mee met een POST van dezelfde site, en met een link die vanaf een andere site wordt gevolgd.

Bevinding 10

“The delete form is accepted with a form token that was issued to a different session.” Het verwijderformulier wordt geaccepteerd met een formuliertoken dat aan een andere is uitgegeven.

Denk na over wie daar gebruik van kan maken. De buur op https://notes.reading.example zit op dezelfde site als de leeslijst, dus een formulier op diens pagina komt aan met Ana’s cookie. De buur kan Ana’s formuliertoken niet lezen. Maar de buur kan een eigen account hebben, inloggen en het token in de eigen pagina lezen. Als de server elk token accepteert dat hij ooit heeft uitgegeven, stuurt de pagina van de buur het eigen token mee met Ana’s cookie, en slaagt de tokencontrole.

Je test voor deze bevinding laat het zien: Ana’s cookie, Bens token, een vertrouwde Origin, en het boek is weg. De controle in delete_from_form() vraagt of het token bekend is. De vraag die ze moet stellen, is of het token hoort bij de sessie die de cookie noemt. csrf_matches(session, sent) beantwoordt precies dat voor één sessie, met hmac.compare_digest, en de functie heeft die sessie al in handen.

Je zou kunnen tegenwerpen dat het formulier van de buur toch al wordt tegengehouden. Zijn Origin is https://notes.reading.example, en de Origin-controle weigert het voordat er naar het token wordt gekeken. Dat klopt vandaag.

De Origin-controle weigert het formulier van de buur al. Waarom zou je bevinding 10 dan nog dichten?

In hoofdstuk 8 schreef je één test met de Origin-controle uitgeschakeld en één met het token uitgeschakeld, om te bewijzen dat elke verdediging de buur in haar eentje tegenhoudt. Bevinding 10 is het geval waarin het token, in zijn eentje, niemand tegenhoudt die een account heeft.

Bevinding 11

“A page on any is told that it may read the service’s answers.” Een pagina op elke willekeurige origin krijgt te horen dat ze de antwoorden van de service mag lezen.

Je test stuurt GET /about met Origin: https://other.example en vindt die origin herhaald in access-control-allow-origin. Hoofdstuk 9 noemde dit reflectie. Het lijkt zorgvuldiger dan *, omdat de header één origin noemt. Het is erger: een browser weigert * te combineren met credentials, en heeft geen bezwaar tegen een gereflecteerde origin. Op GET /me, waar de service credentials toestaat, zou een pagina waarvan de verzoeken Ana’s cookie meedragen dus haar naam kunnen lezen. Met SameSite=Lax is dat de buur op https://notes.reading.example. Een pagina op een andere site stuurt geen cookie mee en leest een antwoord zonder gebruiker erin. POST /tokens staat ook credentials toe, en daar staat alleen de Origin-controle nog tussen de buur en een token dat met haar cookie is gemaakt.

Het herstel zit in cors_headers(). De toestemming gaat alleen naar een origin die in trusted staat, als geheel vergeleken. Die set is state["trusted_origins"], dezelfde die de Origin-controle gebruikt, zodat de twee beslissingen niet uit elkaar kunnen groeien. Houd vary: Origin op elk antwoord, ook op een weigering: het antwoord hangt hoe dan ook van die header af.

Je opdracht

Drie wijzigingen, in deze volgorde: de regel in mini_browser.py, de tokencontrole in de tak van het formulier, en de in cors_headers(). Voer daarna alles uit:

python -m pytest -q

Het resultaat dat je kunt verwachten, is nul fouten: elf aanvalstests en zes modeltests.

Druk daarna op Check my work. Die roept de functies van je model aan met cookies en sites die je niet hebt gezien, en voert tests/test_browser.py ertegen uit. Hij controleert bevinding 10 met twee eigen sessies en bevinding 11 met een eigen vertrouwde set, waarin een origin die alleen begint zoals een vertrouwde, moet worden geweigerd. Daarna voert hij de reeks verzoeken uit het contract twee keer in het geheugen uit: jouw server met een correct model, en een correcte server met jouw model, zodat een fout aan de ene kant zich niet achter de andere kan verschuilen. Dat model is ook een model; het is geen browser. Als laatste wordt alles uit les 2 opnieuw gecontroleerd.

Er is nog niets met een in aanraking gekomen. De volgende les draait de hele service op het .

Opdracht

Herstel de modelbrowser en dicht daarna bevindingen 10 en 11.

In mini_browser.py:

  • Voer python -m pytest tests/test_browser.py -q uit en herstel de ene verzwakte regel waar test_lax_cookie_stays_home_on_a_cross_site_post en test_samesite_stops_a_form_from_another_site naar wijzen. De andere twee fouten daar zijn bevinding 11. Een met SameSite=Lax gaat alleen mee met een cross-site verzoek als het verzoek top-level is en de GET of HEAD is. Met elk same-site verzoek gaat hij nog steeds mee.

In reading_server.py:

  • Bevinding 10: POST /books/delete accepteert een formuliertoken alleen als het het token is van de die de cookie van het verzoek noemt. Vergelijk met csrf_matches(), en lees het token alleen uit de body van het formulier. Elk ander token krijgt 403 en het boek blijft staan.

  • Bevinding 11: cors_headers(request, trusted, *, credentials=False) schrijft access-control-allow-origin alleen voor een die in trusted staat, als geheel vergeleken. Het geeft vary: Origin terug op elk antwoord, ook als het weigert.

Voer python -m pytest -q uit. Alle elf aanvalstests en alle zes modeltests slagen.

Druk daarna op Check my work. Die roept samesite_allows() en cookies_to_send() aan met cookies en sites die je niet hebt gezien, en voert tests/test_browser.py uit tegen je model. Hij post het verwijderformulier met de cookie van de ene sessie en het token van een andere sessie, met het eigen token van de sessie, en met een token in de querystring, en registreert hmac.compare_digest. Hij roept cors_headers() en handle() aan met een eigen vertrouwde set: vertrouwde origins moeten worden toegestaan, en een buur op dezelfde site, een origin die alleen begint zoals een vertrouwde, null en een ontbrekende Origin niet. Hij voert de reeks uit het contract in het geheugen uit met jouw server en een correct model, en met een correcte server en jouw model. Daarna herhaalt hij de controles van les 2, voert hij je elf tests uit tegen je bestanden, en voert hij zijn eigen kopie van de aanvalstests uit hoofdstuk 1 tot en met 10 uit. Niets uit je telt mee.