0%

Wie wat mag, en hoe vaak · oefening

Logs en fouten zonder geheimen

ben probeerde het boek van ana te verwijderen en kreeg een 404. Zes verkeerde wachtwoorden voor ana eindigden in een 429. Wie zou het ooit weten? Je server weigerde beide en legde van geen van beide iets vast.

Een server die weigert, moet zijn weigeringen opschrijven. En wat een server opschrijft, wordt door meer mensen gelezen, en langer bewaard, dan bijna alles wat hij verder maakt: beheerders, supportmedewerkers, back-upsystemen, iedereen die een bestand krijgt om een probleem uit te zoeken. Een log is de makkelijkste plek om een geheim te lekken dat elk ander deel van de server bewaakt.

Wat nooit in een log hoort

  • De van Authorization. Bij een login is het een wachtwoord in . Bij een boekverzoek is het een token dat 15 minuten werkt.

  • Een cookiewaarde. Een is het inloggegeven zodra de login voorbij is.

  • Een wachtwoord of een token in welke andere vorm ook.

  • De querystring. Daar komen tokens, e-mailadressen en zoekwoorden in terecht.

Python heeft een logging voor echte diensten. Deze cursus gebruikt die niet. Hier is de log een tekstregels in state["log"], zodat een test hem kan lezen.

Vijf velden

Eén verzoek, één regel, vijf velden met één spatie ertussen: de tijd in hele seconden, de , het pad, de status en de gebruiker. Met de klok vast op 1000000 zien ben die een token haalt, probeert het boek van ana te verwijderen, en iemand die een login niet haalt, er zo uit:

1000000 POST /login 200 ben
1000000 POST /tokens 200 ben
1000000 DELETE /books/1 404 ben
1000000 POST /login 401 -

log_line(now, request, status, user) geeft zo’n regel terug. int(now) laat de fractie van de tijd vallen. request["target"].partition("?")[0] laat de query vallen: partition splitst tekst bij het eerste ? in drie delen, de tekst ervoor, het ? zelf, en de tekst erna, en [0] is het eerste deel. Als er geen ? is, is het eerste deel de hele tekst.

Waarom niet urlsplit, dat je in with Python gebruikte? Dat parst het hele doel, en sommige doelen weigert het met een fout. urlsplit("http://[") geeft een . Er wordt voor elk verzoek een logregel geschreven, ook voor de vreemde, dus de code die hem schrijft, mag niet kunnen falen. partition werkt op elke tekst.

Wat drukt print("/login?next=/books&token=abc123".partition("?")[0]) af?

Het pad zoals het binnenkwam

Log het pad precies zoals het binnenkwam, nog procent-gecodeerd. Decodeer het niet.

Een pad kan %0A bevatten. Gedecodeerd is dat een regeleinde, en een regeleinde in een logregel begint een nieuwe regel. Een vreemde zou dan een tweede regel naar keuze in je log kunnen schrijven, een die eruitziet als een verzoek dat nooit gebeurd is. Gecodeerd gelaten is %0A drie onschuldige tekens.

Dit is een eerste blik op een patroon waar heel hoofdstuk 7 over gaat: tekst uit een verzoek die ergens terechtkomt waar hij als structuur gelezen wordt.

De oplossing heeft een grens. Andere stuurtekens kunnen nog steeds een methode of een pad bereiken, en een echte log escapet die ook voordat hij een regel schrijft.

Wiens naam

Het vijfde veld is request["user"]. Het project zet het op drie plekken: als een token geverifieerd wordt, als een sessie levend is, en als een login slaagt. Anders is het None, en eindigt de regel op een streepje.

Een mislukte login wordt dus gelogd met een streepje, niet met de naam die werd ingetypt. De naam is niet bewezen. Het is tekst die een vreemde koos, en mensen typen soms per ongeluk hun wachtwoord in het naamveld.

Die keuze heeft een prijs. Voor zes verkeerde wachtwoorden en nog één poging laat de log vijf keer POST /login 401 - zien en daarna twee keer POST /login 429 -. Een beheerder ziet dat er naar een of ander account geraden wordt, en niet welk. Een echte dienst legt de geprobeerde naam ook vast, in een aparte beveiligingslog die veel minder mensen mogen lezen.

Een login mislukt. Waarom eindigt de logregel op een streepje en niet op de naam die gestuurd werd?

Schrijf eerst de aanval

steps.bearer_for(state, "ana", "correct horse") doet twee verzoeken: een login en een POST /tokens. De test roept het aan en controleert daarna twee dingen. De log heeft precies twee regels. En het wachtwoord, de Basic-waarde en de bearer-waarde staan geen van alle in de samengevoegde tekst.

Voer python -m pytest -q uit: 1 failed, 19 passed, met assert 0 == 2. De log is leeg.

De eerste assert staat er met een reden. Een test die alleen naar geheimen zocht, zou slagen op een server die helemaal niets logt.

Fouten zijn ook uitvoer

Een log is één ding wat een server schrijft. Een foutmelding is een ander. Als Python-code faalt, maakt ze een , die je in Python in Practice leerde lezen. Deze eindigt zoals de jouwe in de volgende les zal eindigen, met andere regelnummers:

Traceback (most recent call last):
  File "reading_server.py", line 77, in handle
    response = route(request, state)
  File "reading_server.py", line 169, in route
    for book_id in state["books"]:
                   ~~~~~^^^^^^^^^
KeyError: 'books'

Hij noemt bestanden, , regelnummers en soms waarden. Voor jou is dat de snelste weg naar een bug. Naar een client gestuurd is het een kaart van de binnenkant van je server, overhandigd aan wie de fout veroorzaakte. De client hoort een vaste zin te krijgen en verder niets:

{"error": "the server could not handle this request"}

De traceback hoort op standaardfout, waar de beheerder leest. De volgende les bouwt dat. Vandaag ontsnapt een fout in route() nog uit handle().

Waarom de traceback niet in de body van de 500 sturen, zodat de client hem kan melden?

Je opdracht

  1. Schrijf test_attack_log_holds_no_secret() en zie hem falen.

  2. Schrijf log_line(now, request, status, user): de vijf velden met één spatie ertussen, en - als de gebruiker None is.

  3. Bewaar in handle het antwoord dat route teruggeeft in een , voeg log_line(state["clock"](), request, response["status"], request["user"]) toe aan state["log"], en geef het antwoord terug.

Voer python -m pytest -q uit: 20 passed. Voer daarna python pl_sec.py other-reader en python pl_sec.py guessing opnieuw uit. Elk drukt nu af wat je server in zijn log schreef.

Eén eerlijke opmerking. state["log"] groeit zo lang als de server draait. Een echte dienst schrijft elke regel naar een stream die iets anders bewaart en inkort. Deze lijst is er zodat een test hem kan lezen.

Opdracht

Schrijf voor elk verzoek één logregel, zonder geheim erin.

  1. Schrijf in tests/test_attacks.py test_attack_log_holds_no_secret(). Roep met state = steps.keyed_state() bearer = steps.bearer_for(state, "ana", "correct horse") aan. Voeg de log samen met text = "\n".join(state["log"]). Controleer dat len(state["log"]) == 2, en dat "correct horse", helpers.basic("ana", "correct horse") en bearer geen van alle in text staan. Voer python -m pytest -q uit: deze test moet eerst falen.

  2. Schrijf in reading_server.py log_line(now, request, status, user). Geef de vijf velden terug met één spatie ertussen: int(now), de , request["target"].partition("?")[0], de status, en de gebruiker, of - als de gebruiker None is.

  3. Bewaar in handle het antwoord dat route teruggeeft in een , voeg log_line(state["clock"](), request, response["status"], request["user"]) toe aan state["log"], en geef het antwoord terug.

Voer python -m pytest -q uit tot het eindigt met 20 passed, en klik daarna op Check my work. Die roept log_line aan met eigen verzoeken: een doel met een query, headers die inloggegevens bevatten, een tijd met een fractie, een pad met een procent-gecodeerd regeleinde, en een doel dat urlsplit niet kan parsen. Hij stuurt vijf verzoeken via reading_server.handle(...) onder een klok die hij bestuurt, waaronder een geweigerd verzoek met een token en een mislukte login, verwacht vijf regels, en doorzoekt de log op elk inloggegeven dat hij stuurde. Hij voert je test vier keer uit, tegen servers die hij op de plaats van reading_server.handle en reading_server.sample_state zet: een correcte, waar je test moet slagen; en een die de headers van het verzoek in elke regel schrijft, een die geen regel schrijft, en een die elk verzoek weigert, waar hij moet falen. Niets uit je wordt beoordeeld.