0%

Origins en cross-site verzoeken · oefening

SameSite en de header Origin

Ga terug naar de uitvoer van python pl_sec.py cross-site-delete in les 5. Elk vervalst formulier had een regel die tot nu toe door niets is gebruikt:

  origin:              https://notes.reading.example

visit() voegt een header origin toe aan elk verzoek dat geen GET of HEAD is, zoals een browser doet. Het vervalste formulier zei dus al die tijd waar het vandaan kwam, en je server keek nooit.

Een header die de pagina niet kan zetten

De header is het lezen waard vanwege wie hem schrijft. De browser schrijft Origin, en het script van een pagina mag hem niet zetten. visit() houdt zich aan die regel: het weigert een wanted die het probeert. Bekijk het één keer. In deze regel doet een pagina op de buur zich voor als de leeslijst:

python -c "import visit; visit.visit('https://notes.reading.example', {'kind': 'fetch', 'method': 'POST', 'url': 'https://reading.example/logout', 'headers': {'Origin': 'https://reading.example'}}, [], print)"

De eindigt met deze regel:

ValueError: a page cannot set the Origin header; the browser writes it

Een programma dat geen browser is, kan elke Origin schrijven die het wil. Het heeft geen browser van Ana die haar meestuurt, dus de leugen levert het niets op.

Exact, uit een set

sample_state() bevat sinds het begin van dit hoofdstuk een set: state["trusted_origins"], met één element, "https://reading.example". De controle stelt één vraag: of de van de header in die set zit. Op een set teksten is in alleen waar voor een tekst die van het eerste tot het laatste teken gelijk is aan een element.

Elke lossere vergelijking laat iemand binnen. Hier zijn er vier, elk met een die erdoor komt:

Een vergelijking die goed lijktEen origin die ze binnenlaat
De waarde begint met https://reading.examplehttps://reading.example.other.example
De waarde bevat reading.examplehttps://notreading.example
De waarde staat op dezelfde site als de leeslijsthttps://notes.reading.example, de buur zelf
De host is reading.example, welk schema ookhttp://reading.example

Een server accepteert een Origin die begint met https://reading.example. Welke origin komt binnen?

Een verzoek- bevat elke header als een waarden, omdat een client dezelfde header twee keer kan sturen. Daarom begint origin_allowed(request, trusted) met request["headers"].get("origin", []). Hij geeft False terug, tenzij die lijst precies één waarde heeft. Daarna antwoordt hij of de waarde in trusted zit. Een verzoek met twee headers Origin wordt geweigerd, wat de eerste ook zegt.

Ontbrekend en null

Een browser stuurt in sommige gevallen Origin: null: vanaf een pagina die de opdracht kreeg geen referrer te sturen, vanuit een frame in een sandbox, en vanuit een bestand dat van schijf is geopend. Een oude of ongewone client stuurt helemaal geen Origin. Weiger beide. De hierboven doet dat al: "null" zit niet in de set, en een ontbrekende header is een lijst zonder waarden.

Deze weigering heeft een prijs, en die prijs is een keuze. Een hulpmiddel dat naar deze route post, moet de header sturen. pl_sec.py doet dat, en de tests in tests/test_attacks.py sturen hem sinds het begin van dit hoofdstuk. Het legt ook een later hoofdstuk vast. Als hoofdstuk 10 beveiligingsheaders toevoegt, mag het Referrer-Policy: no-referrer niet zetten, anders zou het eigen formulier van de leeslijst met Origin: null aankomen.

Het model schrijft altijd een echte origin. De null-gevallen komen uit de Fetch- en HTML-standaarden en worden hier niet getoond. Fetch voegt er nog twee toe: een verzoek dat via een andere origin werd doorverwezen, en een POST vanaf een https-pagina naar een http-adres. Een oudere header, Referer, draagt hetzelfde idee en kan op meer manieren ontbreken.

Een POST die door een cookie wordt geautoriseerd, komt aan met Origin: null. Wat doet de server?

Drie verdedigingen, twee aanvallers

Het verwijderformulier heeft nu drie verdedigingen. In dit hoofdstuk zijn twee aanvallers verschenen: een pagina op other.example en een pagina op de buur.

VerdedigingEen formulier van other.exampleEen formulier van notes.reading.example
SameSite=Laxtegengehouden: de cookie blijft thuisniet tegengehouden: dezelfde site, dus de cookie gaat mee
Het token in het formuliertegengehouden: het kan de pagina niet lezentegengehouden: het kan de pagina niet lezen
De controle op Origintegengehouden: niet in de settegengehouden: niet in de set

Houd alle drie, omdat elk op een andere manier faalt: een browser zonder SameSite, een bug in de pagina die het token laat uitlekken, een route die iemand vergeet. Een andere cursus van Python Land, with Python, gebruikt dezelfde controle op Origin om een WebSocket-handshake te bewaken.

Welke verdediging houdt een vervalst formulier van notes.reading.example niet tegen?

Je opdracht

  1. Schrijf origin_allowed(request, trusted) in reading_server.py: precies één waarde voor origin, en die waarde in de set.

  2. Vraag het in de tak "/books/delete" van route, bij een POST, vóór al het andere. Als origin_allowed(request, state["trusted_origins"]) onwaar is, is het antwoord json_error(403, "this request did not come from a page of this service"), en wordt de niet opgezocht.

Voer daarna de tests uit:

python -m pytest -q

De run eindigt nog steeds met 30 passed. Geen test faalt zonder de nieuwe controle, omdat het token het vervalste formulier al tegenhoudt. Dat is een gat in de tests, en het project in les 8 dicht het.

Klik daarna op Check my work.

Opdracht

Controleer de header Origin van het verwijderformulier, exact en tegen een set.

  1. Schrijf in reading_server.py origin_allowed(request, trusted). Lees request["headers"].get("origin", []), een . Geef False terug, tenzij de lijst precies één waarde heeft. Geef daarna terug of die waarde in de set trusted zit. Vergelijk de hele tekst en repareer niets: geen kleine letters, geen slash weggehaald, geen voorvoegsel.

  2. Roep in de tak "/books/delete" van route, bij een POST, origin_allowed(request, state["trusted_origins"]) aan voordat de wordt gelezen. Als die False teruggeeft, is het antwoord json_error(403, "this request did not come from a page of this service"). Anders gaat de tak verder zoals eerst.

Voer python -m pytest -q uit. Het eindigt met 30 passed.

Klik daarna op Check my work. Die roept origin_allowed aan met verzoek- en een eigen vertrouwde set, die twee bevat, waarvan één met een . Die twee moeten worden toegestaan. Deze moeten worden geweigerd: een verzoek zonder de header, met twee headers Origin, met null, met een lege waarde, met een tekst die maar een deel van een vertrouwde origin is, met een ander schema, met een naburige host van dezelfde site, met een vertrouwde host zonder zijn poort, met lookalikes die beginnen of eindigen zoals een vertrouwde origin, met hoofdletters, met een slash aan het einde en met een kale host. Daarna logt hij een eigen gebruiker in en bestuurt hij je server in het geheugen, met een eigen browsermodel. Een formulier van een naburige host dat het juiste token draagt, moet het boek laten staan en een 403 krijgen. Verzoeken met de juiste en het juiste token en zonder Origin, met null of met een lookalike moeten een 403 krijgen en niets veranderen. Een verzoek met een niet-vertrouwde Origin en zonder cookie moet een 403 krijgen en geen 401. Het eigen formulier van de pagina moet nog steeds verwijderen. Niets uit je wordt beoordeeld.