0%

Hoofdstuk 11 · eindproject

Eindproject: maak de leeslijst veilig

Eindproject: Vind elk gat met een test

Tien hoofdstukken lang vertelde elke les je waar het gat zat. Hoofdstuk 3 wees naar de wachtwoordtabel, hoofdstuk 6 naar de eigenaarsregel, hoofdstuk 9 naar één header. Dit hoofdstuk doet dat niet meer.

Het project in dit hoofdstuk is de leeslijst zoals je die na hoofdstuk 10 achterliet, met elf zwakke plekken teruggezet. Niets markeert ze. De code leest als code die werkt, tests/test_attacks.py bevat vijf imports en geen enkele test, en elke route antwoordt nog. In plaats van een les die aanwijst, krijg je één bestand: SECURITY.md. Het beschrijft elke zwakke plek zoals iemand van buiten die zou opmerken, en het noemt nooit een of een regel.

Je werk in deze les is om elk van die elf beschrijvingen om te zetten in een test die faalt. Je dicht nog niets.

Lees het contract

Open SECURITY.md. Het is bevroren: je leest het en bewerkt het nooit. Het heeft drie delen die er vandaag toe doen. Het bestand is in het Engels, dus hieronder staan de Engelse kopjes.

Findings (bevindingen) somt elf symptomen op, elk met de naam die de bijbehorende test moet hebben. Bevinding 7 luidt “Another reader can delete my book with the form on the page.”: een andere lezer kan mijn boek verwijderen met het formulier op de pagina. Die zin zegt niet welke route, welke functie of welke ontbrekende aanroep. Hij zegt wat een mens zou zien.

What a browser sends (wat een browser stuurt) is een tabel voor bevindingen 10 en 11. Die twee gaan over verzoeken die andere pagina’s veroorzaken. Het contract vertelt je ook dat één regel van mini_browser.py is verzwakt, dus je kunt het model pas vanaf les 3 vertrouwen. De tabel zegt wat een correct model beslist, en je mag die verzoeken op basis daarvan met de hand bouwen: helpers.form_request() met een header Cookie en een header Origin, of helpers.make_request() met een Origin.

Must still work (moet blijven werken) somt op wat een oplossing niet kapot mag maken. Ook die heb je vandaag nodig, want een test mag niets eisen wat deze verbiedt.

Een bevinding is een symptoom. Een test is het symptoom precies gemaakt: dit verzoek, deze state, deze assertion. Acht van de elf namen zijn tests die je al eens eerder schreef, in hoofdstuk 2 tot en met 9. De andere drie zijn nieuw, en elk volgt dezelfde vorm: zet de state klaar, voer de aanval uit, controleer dat hij werd geweigerd.

Waarom schrijf je alle elf tests voordat je iets dicht?

Twee bevindingen die je in geen antwoord ziet

Bevindingen 1 en 2 laten geen spoor na in een status, header of body. Een login voor een naam die niet bestaat, krijgt dezelfde 401 als een verkeerd wachtwoord. Een tag die met == wordt vergeleken, geeft hetzelfde antwoord als een die veilig wordt vergeleken. Het verschil zit in het werk dat de server doet.

Precies dit heb je al eerder getest. helpers.count_scrypt() telt elke aanroep van hashlib.scrypt vanaf het moment dat je het aanroept, en noteert van elke aanroep de kosten n. helpers.count_compare_digest() doet hetzelfde voor hmac.compare_digest. De test van hoofdstuk 3 voor een onbekende gebruiker en de test van hoofdstuk 2 voor verify() rusten allebei op een telling. Er wordt niets getimed, hier niet en nergens in deze cursus: een telling is exact, en een stopwatch niet.

Bevinding 1 is in geen enkel antwoord te zien. Hoe ziet een test haar zonder tijd te meten?

Een test die iets bewijst

Een rode test is maar de helft van het bewijs. Dezelfde test moet slagen zodra de bevinding is gedicht, en dat betekent dat hij moet slagen tegen een correcte server. Twee fouten breken dit, in tegengestelde richtingen.

De eerste is een test die het gat nooit vindt. Neem bevinding 9. Een voor de hand liggende eerste poging vraagt om /covers/..%2Fsecret.txt en verwacht een 404. Voer hem uit, en hij slaagt tegen de code in deze map, omdat die code een naam met twee punten al weigert. De test is groen en het gat is er nog steeds. Een covernaam kan de map op meer dan één manier verlaten, en hoofdstuk 7 noemde ze.

Een test voor bevinding 9 vraagt om ../secret.txt, krijgt None en slaagt tegen de begincode. Wat zegt dat je?

De tweede fout is een test die te veel vraagt. Een test voor bevinding 5 die een token bewerkt en het daarna opnieuw ondertekent met steps.TEST_KEYS, faalt tegen elke server, ook tegen correcte, omdat geen aanvaller die sleutel heeft en een server moet accepteren wat zijn eigen sleutel heeft ondertekend. Begin elke test met zijn , zoals je sinds hoofdstuk 1 doet: controleer eerst dat het eerlijke verzoek werkt, en voer dan de aanval uit.

Vier bevindingen vragen elk nog een extra gedachte. Houd bij bevinding 4 de klok onder de absolute limiet, zodat alleen de inactiviteitslimiet de kan beëindigen. Roep bij bevinding 6 signing.load_keys() aan met een die je zelf bouwt, zoals in hoofdstuk 5. Roep reading_server.main() niet aan vanuit een test: zolang de bevinding open is, start het een server en wacht het op verbindingen. Stuur bij bevinding 10 een vertrouwde Origin, anders antwoordt de Origin-controle al voordat er ooit naar het formuliertoken wordt gekeken. Voor bevinding 7 en bevinding 10 heb je een formuliertoken nodig: het staat in de sessie, op state["sessions"][session_id]["csrf"], en de sessie-id is het deel na = in het paar dat steps.login_cookie() teruggeeft.

Je opdracht

Schrijf de elf tests in tests/test_attacks.py, met de namen die SECURITY.md geeft. Bereik de code via zijn , zoals het commentaar bovenaan het bestand zegt: reading_server.handle(...), signing.verify(...), passwords.check_login(...). Voer ze daarna uit:

python -m pytest tests/test_attacks.py -q

Het resultaat waar je in deze les naar streeft, is ongewoon:

11 failed

Een gewone python -m pytest -q toont 15 failed, omdat vier tests van het model in tests/test_browser.py ook rood zijn. Die zijn voor les 3. Lees elke fout voordat je verdergaat. Een test die faalt op zijn voorwaarde, of met een in zijn eigen opzet, is rood om de verkeerde reden.

Druk daarna op Check my work. Die kijkt helemaal niet naar de code in deze map. Hij houdt een correcte kopie van de service bij en elf kopieën die elk één bevinding hebben, en voert je tests daartegen uit: elke test moet slagen tegen de correcte, en elke test moet falen tegen de kopie met zijn bevinding. De map bevat dus nog steeds de zwakke server als deze les klaar is. De volgende les begint daarmee.

Opdracht

Schrijf de elf aanvalstests die SECURITY.md noemt, in tests/test_attacks.py. Verander in deze les verder niets.

  • Geef elke test precies de naam uit de tabel van het contract, van test_attack_unknown_user_costs_the_same_work tot en met test_attack_another_origin_cannot_read_the_list.

  • Elke test voert zijn aanval uit en controleert dat de aanval wordt geweigerd. Daardoor faalt elke test tegen de code in deze map, en zou hij slagen zodra zijn bevinding is gedicht.

  • Controleer eerst het eerlijke geval als dat er is: de login werkt, het token wordt geaccepteerd, de cover wordt geserveerd. Val daarna aan.

  • Bereik de service via zijn : reading_server.handle(...), signing.verify(...), signing.load_keys(...), passwords.check_login(...). Schrijf geen from reading_server import ..., en open geen en start pl_sec.py niet vanuit een test.

  • Bevindingen 1 en 2 zijn alleen te zien aan verricht werk. Gebruik helpers.count_scrypt() en helpers.count_compare_digest().

  • Bouw voor bevindingen 10 en 11 de verzoeken met de hand op basis van de tabel “What a browser sends” in SECURITY.md. Het formuliertoken van een is state["sessions"][session_id]["csrf"].

  • Elke POST naar /login, /logout, /prefs, /tokens en /books/delete heeft Origin: https://reading.example nodig, en dat is steps.OWN. steps.login_cookie() en steps.bearer_for() sturen het voor je mee.

Voer python -m pytest tests/test_attacks.py -q uit in de . Elf fouten is het resultaat waar je naar streeft.

Druk daarna op Check my work. Die leest je testbestand op de elf namen en op imports die een verwisselde server zouden verbergen. Hij voert alle elf tests uit tegen een correcte kopie van de service, waar elke test moet slagen, en voert daarna elke test afzonderlijk uit tegen een kopie die alleen de bevinding van die test heeft, waar hij moet falen op een assertion. De code in deze map, die nog elke zwakke plek bevat, wordt niet beoordeeld, en niets uit je terminal telt mee.