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 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
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 urlsplit("http://[") geeft een 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
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,
{"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
Schrijf
test_attack_log_holds_no_secret()en zie hem falen.Schrijf
log_line(now, request, status, user): de vijf velden met één spatie ertussen, en-als de gebruikerNoneis.Bewaar in
handlehet antwoord datrouteteruggeeft in een, voeg log_line(state["clock"](), request, response["status"], request["user"])toe aanstate["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.
Schrijf in
tests/test_attacks.pytest_attack_log_holds_no_secret(). Roep metstate = steps.keyed_state()bearer = steps.bearer_for(state, "ana", "correct horse")aan. Voeg de log samen mettext = "\n".join(state["log"]). Controleer datlen(state["log"]) == 2, en dat"correct horse",helpers.basic("ana", "correct horse")enbearergeen van alle intextstaan. Voerpython -m pytest -quit: deze test moet eerst falen.Schrijf in
reading_server.pylog_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 gebruikerNoneis.Bewaar in
handlehet antwoord datrouteteruggeeft in een, voeg log_line(state["clock"](), request, response["status"], request["user"])toe aanstate["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