Origins en cross-site verzoeken · oefening
Vervals een verzoek vanaf een naburige site
Les 4 eindigde met een formulier dat een pagina op een naburige host naar je server post. Deze les verstuurt het. Voer het scenario uit:
python pl_sec.py cross-site-delete
Het start je server op deze computer en speelt een browser met je mini_browser.py. De eerste regels zeggen dat de namen alleen in het model bestaan en dat elke byte naar deze computer gaat. Ana logt in vanaf de eigen pagina van de leeslijst. Daarna posten twee andere pagina’s het verwijderformulier voor boek 1:
--- a page on https://other.example posts the form: id=1 to /books/delete ---
cookie sent: no
origin: https://other.example
the server answered: 401 (pl_sec sees this; the page does not)
book ids left: 1, 2
--- a page on https://notes.reading.example posts the same form ---
cookie sent: yes
origin: https://notes.reading.example
the server answered: 303 (pl_sec sees this; the page does not)
book ids left: 2
Het formulier van other.example vertrok zonder de notes.reading.example droeg de cookie. De server antwoordde 303, wat hij ook zijn eigen pagina antwoordt, en boek 1 is weg. In het laatste deel van de uitvoer post de eigen pagina van de leeslijst haar formulier, en dat werkt ook. Je server kan het tweede formulier niet van het derde onderscheiden.
Geen van beide pagina’s zag een status. pl_sec.py drukt hem af omdat het de browser speelt. Een pagina die een formulier post, leest het antwoord nooit.
Lees visit.py
Open visit.py. Het wordt meegeleverd, en je verandert het nooit. visit(page_origin, wanted, jar, send) doet één verzoek zoals een browser dat zou doen, en vraagt bij elke beslissing je regels om raad. Lees het op vijf punten:
Wat
wantedbevat. Het is een. "kind"is"form","link","image"of"fetch". Daarna komen"url","method"en optioneel"headers"en"body". De eerste helft van deweigert wat een soort niet kan. Een formulier stuurt GET of POST, en de enige header die het kan zetten, is een content type voor formulieren. Waar je
cookies_to_sendwordt aangeroepen. Dat gebeurt tegen het einde, mettop_levelwaar voor een formulier en voor een link. De cookies die hij teruggeeft, worden de headercookie.Waar
originwordt toegevoegd. Een paar regels daarboven, bij elk verzoek dat geen GET of HEAD is. Les 7 gebruikt deze header.Wat
sendis. Het is een functie die je meegeeft, zoals je in Networks with Python functies doorgaf. Ze brengt één verzoek naar zijn server en geeft het antwoord terug.steps.send_to(state)bouwt er een diereading_server.handle()in het geheugen aanroept.Wat er terugkomt. Weer een dict.
"requests"somt elk verzoek op dat de browser verliet."response"is wat het script van de pagina mag lezen. Voor een formulier is datNone, en"blocked"zegt waarom.
Sla het blok over dat met hoofdstuk 9 is gemarkeerd.
Wat is send in visit(page_origin, wanted, jar, send)?
Hoofdstuk 4 hield al één aanvaller tegen
Het eerste resultaat van het scenario verdient een test. Open tests/test_browser.py. Elke test van deze les begint op dezelfde manier: Ana logt in vanaf de eigen pagina van de leeslijst, zodat de jar haar cookie bevat.
state = reading_server.sample_state()
jar = []
send = steps.send_to(state)
wanted = {
"kind": "fetch",
"method": "POST",
"url": steps.OWN + "/login",
"headers": {"Authorization": helpers.basic("ana", "correct horse")},
}
result = visit.visit(steps.OWN, wanted, jar, send)
assert result["response"]["status"] == 200
assert len(jar) == 1
Daarna post een pagina op een andere site het formulier:
form = {
"kind": "form",
"method": "POST",
"url": steps.OWN + "/books/delete",
"headers": {"Content-Type": "application/x-www-form-urlencoded"},
"body": b"id=1",
}
result = visit.visit(steps.OTHER, form, jar, send)
Schrijf test_samesite_stops_a_form_from_another_site uit deze twee stukken, en sluit af met de assertie dat "cookie" not in result["requests"][0]["headers"]. Hij slaagt bij de eerste run. De vlag SameSite=Lax uit hoofdstuk 4 houdt de cookie thuis, en je model laat dat zien.
Bij een echte aanval opent Ana de andere pagina terwijl ze is ingelogd. Hier speelt de test haar rol: hij logt haar in en post daarna het formulier voor haar.
De buur
notes.reading.example is een host waar lezers eigen pagina’s publiceren, dus iedereen kan daar een formulier neerzetten. Het is een andere Lax laat de cookie meegaan, en Strict zou dat ook doen.
Schrijf de aanvalstest in tests/test_attacks.py en noem hem test_attack_another_origin_cannot_delete_a_book. Log Ana in, post hetzelfde formulier vanaf steps.NOTES en controleer twee dingen in deze volgorde. Eerst dat "cookie" in de headers van het verzoek staat. Zonder die regel zou de test kunnen slagen omdat de cookie thuisbleef. Daarna dat 1 in state["books"]. De test mag state lezen. De vervalsende pagina kan dat niet.
Voer alles uit:
python -m pytest -q
De laatste regel begint met 1 failed, 29 passed, en de fout is je tweede assertie: boek 1 is weg. De test doet zijn werk. Een aanvalstest faalt zolang de aanval werkt. Het commentaar bovenaan het bestand noemt deze test al als een test die tot les 6 hoort te falen.
De aanval heet
Waarom werkt de vervalsing vanaf notes.reading.example en niet vanaf other.example?
Wat nooit in gevaar was
De /books kijken niet naar de cookie. Ze vragen om een bearer-token in een header Authorization. Een formulier kan die header niet zetten, en geen browser stuurt uit zichzelf een token mee. Dezelfde pagina kan DELETE /books/1 dus niet vervalsen. De vervalsing werkt waar een cookie het bewijs van identiteit is.
Waarom kan dezelfde pagina DELETE /books/1 op de JSON-route niet vervalsen?
Je opdracht
Schrijf de twee tests, elk onder het commentaar van hoofdstuk 8 in zijn bestand:
test_samesite_stops_a_form_from_another_siteintests/test_browser.py. Die slaagt.test_attack_another_origin_cannot_delete_a_bookintests/test_attacks.py. De laatste assertie ervan faalt vandaag. De volgende les is de eerste die vraagt dat hij slaagt.
Je verandert in deze les niets in reading_server.py. Beide tests bereiken de code via de visit.visit(...) en reading_server.sample_state(), zonder regel from ... import.
Klik daarna op Check my work.
Opdracht
Schrijf twee tests: een die SameSite=Lax aan het werk laat zien, en een die een verzoek vanaf een naburige host vervalst en faalt.
Beide tests beginnen met ana inloggen via het model: jar = [], send = steps.send_to(state) met state = reading_server.sample_state(), en visit.visit(steps.OWN, wanted, jar, send) met een wanted waarvan "kind" gelijk is aan "fetch", "method" aan "POST", "url" aan steps.OWN + "/login", en waarvan "headers" de "Authorization": helpers.basic("ana", "correct horse") bevat. Controleer dat result["response"]["status"] == 200.
Beide posten daarna het verwijderformulier met visit.visit(page, form, jar, send). Het formulier is {"kind": "form", "method": "POST", "url": steps.OWN + "/books/delete", "headers": {"Content-Type": "application/x-www-form-urlencoded"}, "body": b"id=1"}.
Schrijf in
tests/test_browser.py, onder het commentaar van hoofdstuk 8,test_samesite_stops_a_form_from_another_site(). Post het formulier vanafsteps.OTHERen controleer dat"cookie" not in result["requests"][0]["headers"]. Voerpython -m pytest -q tests/test_browser.pyuit. De test slaagt.Schrijf in
tests/test_attacks.py, onder het commentaar van hoofdstuk 8,test_attack_another_origin_cannot_delete_a_book(). Post het formulier vanafsteps.NOTES. Controleer dat"cookie" in result["requests"][0]["headers"], en daarna dat1 in state["books"]. Voerpython -m pytest -quit. Deze test faalt bij zijn laatste assertie: het boek is weg. Hij hoeft pas in de volgende les te slagen.
Verander niets in reading_server.py.
Klik daarna op Check my work. Die selecteert de twee tests op naam en draait elk een paar keer, in het geheugen. Je test van het model draait met een model waarvan de regels kloppen, waar hij moet slagen, en met een model dat een Lax-visit.visit aanroepen en reading_server.handle via de