0%

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 van Ana. De server antwoordde 401, en beide boeken bleven. Hetzelfde formulier van 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:

  1. Wat wanted bevat. Het is een . "kind" is "form", "link", "image" of "fetch". Daarna komen "url", "method" en optioneel "headers" en "body". De eerste helft van de weigert 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.

  2. Waar je cookies_to_send wordt aangeroepen. Dat gebeurt tegen het einde, met top_level waar voor een formulier en voor een link. De cookies die hij teruggeeft, worden de header cookie.

  3. Waar origin wordt toegevoegd. Een paar regels daarboven, bij elk verzoek dat geen GET of HEAD is. Les 7 gebruikt deze header.

  4. Wat send is. 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 die reading_server.handle() in het geheugen aanroept.

  5. 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 dat None, 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 en dezelfde site. 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 . De naam stamt uit 2001, jaren voordat browsers de grens trokken tussen een site en een origin die je in les 2 leerde, en dit verzoek verliet de site nooit. “Cross-origin” zou het beter beschrijven, maar de naam bleef hangen.

Waarom werkt de vervalsing vanaf notes.reading.example en niet vanaf other.example?

Wat nooit in gevaar was

De -routes onder /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_site in tests/test_browser.py. Die slaagt.

  • test_attack_another_origin_cannot_delete_a_book in tests/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 , zoals in 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"}.

  1. Schrijf in tests/test_browser.py, onder het commentaar van hoofdstuk 8, test_samesite_stops_a_form_from_another_site(). Post het formulier vanaf steps.OTHER en controleer dat "cookie" not in result["requests"][0]["headers"]. Voer python -m pytest -q tests/test_browser.py uit. De test slaagt.

  2. Schrijf in tests/test_attacks.py, onder het commentaar van hoofdstuk 8, test_attack_another_origin_cannot_delete_a_book(). Post het formulier vanaf steps.NOTES. Controleer dat "cookie" in result["requests"][0]["headers"], en daarna dat 1 in state["books"]. Voer python -m pytest -q uit. 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- meestuurt met een cross-site formulier-POST, waar hij moet falen. Je aanvalstest draait tegen een server zonder verdediging, waar hij moet falen, en tegen een server die een formulier van een andere weigert, waar hij moet slagen. Hij moet ook visit.visit aanroepen en reading_server.handle via de bereiken. Niets uit je wordt beoordeeld.