Origins en cross-site verzoeken · oefening
Een token dat de andere pagina niet kan kennen
Je aanvalstest is rood. Kijk wat delete_from_form te zien krijgt van het vervalste verzoek en van het eerlijke: een POST voor /books/delete, de id=1 zegt. Alles waar de
Een geheim in de pagina
Les 4 gaf je de regel die dit mogelijk maakt. Een pagina op notes.reading.example mag verzoeken naar de leeslijst sturen, en mag de antwoorden niet lezen. Ze kan GET / dus niet lezen, en ook geen
Die waarde is een token: een willekeurige tekst uit secrets.token_urlsafe(32), de functie die ook je page.html heeft het veld al, en render_page(books, user, token="") vult het al, geëscapet zoals elke andere waarde. Tot nu toe gaf niemand een token mee, dus het veld was leeg. Als er een formulier binnenkomt, vergelijkt de server het veld ervan met het token in de sessie.
the server the browser
session of ana holds the token T
GET / -> page with csrf = T -> page of reading.example
posts id=1&csrf=T -> T matches: 303
page of notes.reading.example
may not read that page
posts id=1 -> no token: 403
In woorden: de server bewaart het token T in de sessie van Ana en schrijft het in de pagina die hij haar serveert. De eigen pagina van de leeslijst post T terug met het formulier, en het boek wordt verwijderd. Een pagina op de buur kan de pagina van de leeslijst niet lezen. Haar formulier draagt geen T, en de server antwoordt 403.
Waarom kan een pagina op notes.reading.example niet het juiste token in haar vervalste formulier zetten?
Gebonden aan de sessie, vernieuwd bij het inloggen
Er is één token per sessie, en het wordt gemaakt waar de sessie wordt gemaakt: in start_session. Een token dat de server voor elke willekeurige sessie accepteerde, zou niets waard zijn. De aanvaller heeft ook een sessie, en kan een token lezen in een eigen pagina.
Het token daar maken heeft een tweede effect. In hoofdstuk 4 schreef je rotate_session, zodat een login de oude sessie beëindigt en een nieuwe begint, en alleen de paginagrootte kopieert. Een login krijgt dus een nieuw token zonder één extra regel code. De reden is dezelfde die je kent van de sessie-id: een token dat de login overleefde, zou een token zijn dat de houder van de bezoekerssessie al kende.
Een server accepteert elk token dat in een willekeurige sessie bestaat. Wat kan Ben doen?
Vergelijk het als een geheim
Het token is een geheim dat de server met één verwachte waarde vergelijkt, dus de vergelijking gaat via hmac.compare_digest, zoals in hoofdstuk 2. Eén detail is nieuw. Het veld bevat wat de client ook maar stuurde, en compare_digest weigert tekst die geen ASCII is:
hmac.compare_digest("é", "abc")
TypeError: comparing strings with non-ASCII characters is not supported
Binnen je server zou die
hmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8"))
Een formulier zonder het veld wordt vergeleken als lege tekst, fields.get("csrf", ""), en faalt zoals elk ander fout token.
De weigering is een 403 en geen 401. De server weet wiens sessie dit is, en opnieuw inloggen zou niet helpen. Hij weigert dit ene verzoek.
Een ingelogde sessie post het formulier zonder token. Welke status?
Alleen uit de body van het formulier
Lees het token uit de body van het formulier en nergens anders vandaan. Een token dat de server ook in de querystring accepteert, zoals in /books/delete?csrf=..., belandt in logs, in de geschiedenis van de browser en in links die mensen delen.
Waarom wordt een token in ?csrf=... niet geaccepteerd?
Je opdracht
Breng vijf wijzigingen aan. Vier ervan zitten in reading_server.py:
Voeg in
start_session"csrf": secrets.token_urlsafe(32)toe aan de sessie-. Geef in de tak
"/"vanroutesession["csrf"]als derdeaan render_pagemee.Schrijf
csrf_matches(session, sent)met de vergelijking hierboven. Schrijf de aanroep alshmac.compare_digest(...).Geef in
delete_from_form, nadat het formulier is gelezen,json_error(403, "the form did not carry this session's token")terug, tenzijcsrf_matches(session, fields.get("csrf", "")).Haal in het commentaar bovenaan
tests/test_attacks.pyje test uit les 5 van demet tests die horen te falen.
Voer daarna de tests uit:
python -m pytest -q
De run eindigt met 30 passed: het vervalste formulier draagt geen token, en het boek blijft.
Het token heeft grenzen. Het beschermt het formulier van deze pagina, en het helpt niet als het script van een aanvaller in de pagina zelf draait. Zo’n script buiten de deur houden is de taak van het escapen dat je in hoofdstuk 7 schreef. Twee varianten hebben namen die je kunt tegenkomen, tokens per verzoek (per-request tokens) en double-submit cookies, en deze cursus behandelt geen van beide.
Nog één vervalsing verdient een paar zinnen: een vervalste login. Een pagina die de browser van Ana kon laten inloggen met de eigen inloggegevens van de aanvaller, zou haar als de aanvaller laten inloggen. Hier heeft een login een Basic-header nodig met de eigen naam en het eigen wachtwoord van de aanvaller, en een formulier kan die header niet zetten. Volgens de standaarden kan een browser er toch toe worden gebracht er een te sturen: een formulier waarvan het adres een gebruikersnaam en wachtwoord bevat, zoals https://ben:pw@reading.example/login, kan de 401 van de server daarmee beantwoorden, en sommige browsers waarschuwen eerst. Het project in les 8 zet de controle op Origin op /login, en die controle weigert zo’n verzoek in beide gevallen. Lees dit niet als “Basic-authenticatie kan niet worden vervalst”: zodra een browser zijn gebruiker om Basic-inloggegevens heeft gevraagd, onthoudt hij die en stuurt hij ze mee met latere verzoeken, zoals hij met een cookie doet.
Klik daarna op Check my work.
Opdracht
Geef elke
Voeg in
start_sessioninreading_server.py"csrf": secrets.token_urlsafe(32)toe aan de sessie-. Roep in de tak
"/"vanrouterender_page(books, session["user"], session["csrf"])aan.Schrijf
csrf_matches(session, sent). Die geefthmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8"))terug: alleenTruealssenthet token van deze sessie is. Roepcompare_digestvia deaan, geschreven als hmac.compare_digest(...).Geef in
delete_from_form, nadat het formulier is gelezen en voordat er naar deidwordt gekeken,json_error(403, "the form did not carry this session's token")terug, tenzijcsrf_matches(session, fields.get("csrf", "")). Lees het token alleen uit de body van het formulier.Haal in het commentaar bovenaan
tests/test_attacks.pytest_attack_another_origin_cannot_delete_a_bookweg uit de tests die horen te falen.
Voer python -m pytest -q uit tot het eindigt met 30 passed.
Klik daarna op Check my work. Die logt eigen gebruikers in en bestuurt je server in het geheugen, met een eigen browsermodel. Een nieuwe sessie moet een token van minstens 22 tekens bevatten dat uit secrets komt, via de module aangeroepen, en twee sessies moeten verschillende tokens bevatten. De pagina van GET / wordt gelezen met de HTML-parser van Python: elk verwijderformulier moet het token van de sessie in zijn veld csrf dragen, en het eigen formulier van de pagina moet nog steeds verwijderen. Daarna worden formulieren gepost vanaf een naburige host die de controle aan state["trusted_origins"] heeft toegevoegd: met een fout token, het token van een andere sessie, geen token, een leeg token, een token dat geen ASCII is, en het juiste token in de querystring. Elk daarvan moet het boek laten staan en een 403 krijgen. Een login die binnenkomt met de reading_server.hmac.compare_digest gezet: de vergelijking moet erdoorheen gaan. Als laatste wordt je aanvalstest uit les 5 één keer tegen je eigen server gedraaid en moet hij slagen. Niets uit je