0%

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 van Ana en een body die id=1 zegt. Alles waar de naar kijkt, is in beide gelijk. Om ze uit elkaar te houden, heeft de server iets nodig wat zijn eigen pagina heeft en wat de andere pagina niet kan krijgen.

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 je server in die pagina schrijft. De werkt nu voor de server.

Die waarde is een token: een willekeurige tekst uit secrets.token_urlsafe(32), de functie die ook je maakt, hier met 32 bytes. De server bewaart het in de sessie en schrijft het in een verborgen veld van elk verwijderformulier. 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 een vervalst formulier in een 500 veranderen. Codeer dus beide kanten en vergelijk bytes:

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:

  1. Voeg in start_session "csrf": secrets.token_urlsafe(32) toe aan de sessie-.

  2. Geef in de tak "/" van route session["csrf"] als derde aan render_page mee.

  3. Schrijf csrf_matches(session, sent) met de vergelijking hierboven. Schrijf de aanroep als hmac.compare_digest(...).

  4. Geef in delete_from_form, nadat het formulier is gelezen, json_error(403, "the form did not carry this session's token") terug, tenzij csrf_matches(session, fields.get("csrf", "")).

  5. Haal in het commentaar bovenaan tests/test_attacks.py je test uit les 5 van de met 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 een token, schrijf het in het formulier van de pagina en weiger een verwijderformulier dat het niet draagt.

  1. Voeg in start_session in reading_server.py "csrf": secrets.token_urlsafe(32) toe aan de sessie-.

  2. Roep in de tak "/" van route render_page(books, session["user"], session["csrf"]) aan.

  3. Schrijf csrf_matches(session, sent). Die geeft hmac.compare_digest(sent.encode("utf-8"), session["csrf"].encode("utf-8")) terug: alleen True als sent het token van deze sessie is. Roep compare_digest via de aan, geschreven als hmac.compare_digest(...).

  4. Geef in delete_from_form, nadat het formulier is gelezen en voordat er naar de id wordt gekeken, json_error(403, "the form did not carry this session's token") terug, tenzij csrf_matches(session, fields.get("csrf", "")). Lees het token alleen uit de body van het formulier.

  5. Haal in het commentaar bovenaan tests/test_attacks.py test_attack_another_origin_cannot_delete_a_book weg 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 van een bezoekerssessie, moet eindigen met een nieuw token. Er wordt een opnemende op de plaats van 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 wordt beoordeeld.