0%

Eindproject: maak de leeslijst veilig · eindproject

Eindproject: Dicht de gaten aan de serverkant

Je hebt elf rode tests en een server waarvan je de zwakke plekken nog niet in de code hebt gezien. Deze les dicht de eerste negen. Ze zitten allemaal aan de kant van de server: in reading_server.py, signing.py en passwords.py. Bevindingen 10 en 11 gaan over verzoeken van andere pagina’s, en die wachten op de volgende les.

De is de regressielus uit Python in Practice, één bevinding tegelijk:

  1. Voer één test uit en lees zijn fout.

  2. Zoek de code waar de fout naar wijst.

  3. Verander die.

  4. Voer alle tests uit, om te zien dat de test die je wilde groen werd en dat niets anders rood werd.

python -m pytest tests/test_attacks.py -q -k form_cannot_delete

-k selecteert de tests waarvan de naam die tekst bevat. Zonder die optie draaien ze alle elf.

Van een symptoom naar een regel

Hier is het hele pad één keer, voor bevinding 7. De andere acht zijn voor jou.

Het symptoom is “Another reader can delete my book with the form on the page.” Je test logt Ben in, post het formulier voor boek 1 met Bens eigen en formuliertoken, en verwacht een 404. Hij faalt:

E       assert 303 == 404

Een 303 is het antwoord op een verwijdering die gelukt is. Het verzoek kwam dus onderweg door elke controle: de methode, de Origin, de , het formulier, het formuliertoken en de id. Het contract geeft die volgorde in zijn routesectie, en de volgende controle daarin is de eigenaar.

Het formulier post naar /books/delete. In route() vindt die tak de sessie en roept hij delete_from_form() aan. Lees die naast de regel van hoofdstuk 6: een verzoek wordt geweigerd tenzij een regel het toestaat. De functie zoekt het boek op, controleert dat de id bestaat, en verwijdert het. Ze vraagt nooit wie de eigenaar van het boek is. DELETE /books/1 met een token vraagt het aan may_change() via item_refusal(). Het formulier is een tweede pad naar dezelfde verwijdering, en het sloeg die vraag over. Dat was de les van hoofdstuk 6: een eigenaarsregel die op het ene pad wordt gecontroleerd en op het andere niet, is geen regel.

De oplossing is één , en de functie die je moet aanroepen, bestaat al.

Let op wat de regel vond. Het was geen zoektocht naar “bug”. Het was de fout van de test, de volgorde van de controles in het contract, en één regel die je al kent. Elk van de andere bevindingen geeft zich gewonnen aan diezelfde drie dingen. De regel staat in het hoofdstuk dat de bevinding voor het eerst dichtte, en de functie die je toen schreef, staat meestal nog in het bestand, alleen wordt ze niet meer aangeroepen of is ze niet meer compleet.

Sluit de deur niet voor iedereen

Een bevinding is gedicht als haar aanval wordt geweigerd en elk eerlijk verzoek nog werkt. De tweede helft raak je makkelijk kwijt. Neem bevinding 9. Deze wijziging laat haar test meteen slagen:

def safe_cover_path(folder, name):
    return None

Geen naam kan de map nu nog verlaten. Er wordt ook geen cover meer geserveerd, en SECURITY.md zegt onder “Must still work” dat GET /covers/kindred.txt de cover teruggeeft. Check my work leest die ook. Hij logt lezers in en telt de wachtwoordhashes, houdt een sessie in leven door haar elk half uur te gebruiken, verwijdert het eigen boek van een lezer met diens eigen formuliertoken, voegt een boek toe met alle vijf velden, en vraagt om een cover. Een oplossing die een van die dingen weigert, wordt net zo duidelijk gemeld als een open bevinding.

Een wijziging laat safe_cover_path() voor elke naam None teruggeven. De test voor bevinding 9 slaagt. Is de bevinding gedicht?

Dezelfde zorg geldt voor de status die een weigering krijgt. Het contract antwoordt 404 voor een boek dat niet van de aanvrager is, zodat het antwoord de id niet bevestigt, en 422 voor een body die een veld noemt dat de client niet mag zetten. Een oplossing die 403 antwoordt, of die het veld stilletjes laat vallen, dicht het gat en breekt het contract.

Je opdracht

Dicht bevindingen 1 tot en met 9, in de volgorde van het contract. Voer na elke bevinding alle tests uit:

python -m pytest tests/test_attacks.py -q

Het aantal fouten moet elke keer met één dalen:

Na bevindingTe verwachten fouten
110
29
38
47
56
65
74
83
92

Als het aantal met twee daalt, lees de diff dan opnieuw: één wijziging dichtte iets wat je niet wilde aanraken, en je wilt weten waarom. Als het niet daalt, is de foutmelding van die ene test de plek om te kijken.

Twee hints over waar dingen staan. Bevindingen 1 en 2 zitten niet in reading_server.py: het werk van een login gebeurt in passwords.py, en een tag wordt vergeleken in signing.py. Bevindingen 5 en 6 zitten ook in signing.py. Al het andere zit in reading_server.py.

Na deze les falen er nog twee tests. Welke, en waarom is dat te verwachten?

Druk daarna op Check my work. Die roept je functies en handle() aan met gebruikers, boeken, een sleutel en kloktijden die je niet hebt gezien, één keer voor elk van de negen bevindingen, en kijkt niet naar hoe een oplossing is geschreven, alleen naar wat de code doet. Hij controleert elke rij van “Must still work”. Hij voert je eigen tests voor bevindingen 1 tot en met 9 uit tegen je bestanden. En hij voert zijn eigen kopie uit van de aanvalstests die je in hoofdstuk 1 tot en met 10 schreef, omdat het testbestand in dit hoofdstuk leeg begon en niets anders die eerdere rijen bewaakt.

Opdracht

Dicht bevindingen 1 tot en met 9 van SECURITY.md in reading_server.py, signing.py en passwords.py, zonder iets onder “Must still work” kapot te maken.

Na je wijzigingen:

  1. Kost een login voor een naam die niet bestaat één aanroep van hashlib.scrypt met de gebruikelijke kosten, net als een verkeerd wachtwoord, en krijgt hij hetzelfde antwoord.

  2. Beslist signing.verify() via hmac.compare_digest.

  3. Krijgt een login altijd een nieuwe . De sessie die de bezoeker had, wordt verwijderd, en de paginagrootte ervan blijft bewaard.

  4. Wordt een sessie die langer dan IDLE_LIMIT niet is gebruikt, geweigerd en verwijderd. Een sessie die in gebruik is, duurt nog steeds tot ABSOLUTE_LIMIT.

  5. Wordt een token geweigerd als een deel van zijn payload is bewerkt, exp inbegrepen. Een token dat de server heeft gemaakt, wordt geaccepteerd tot exp.

  6. Werpt signing.load_keys() op als READING_KEY ontbreekt, leeg is of korter is dan 32 bytes, en drukt main() dan één regel af op standaardfout en geeft het 1 terug.

  7. Antwoordt het verwijderformulier 404 voor een boek dat niet van de aanvrager is, en blijft het boek staan.

  8. Krijgt een boekbody die owner of id noemt het antwoord 422 met de sleutel in de melding, en wordt er niets opgeslagen.

  9. Geeft safe_cover_path() alleen een pad terug voor een bestand binnen de opgeloste covermap.

Laat bevindingen 10 en 11, en mini_browser.py, liggen voor de volgende les.

Voer na elke bevinding python -m pytest tests/test_attacks.py -q uit. Het aantal fouten gaat van 11 naar 2.

Druk daarna op Check my work. Die controleert elk van de negen bevindingen via handle() en je , onder een eigen state met gebruikers, boeken, een sleutel en kloktijden die je niet hebt gezien, en telt aanroepen van hashlib.scrypt en hmac.compare_digest waar een bevinding over verricht werk gaat. Hij controleert de rijen van “Must still work”: één wachtwoordhash voor een correcte login, een sessie die in leven blijft zolang ze wordt gebruikt, een token onder de vorige sleutel, een lezer die zijn eigen boek verwijdert, een boek met alle vijf velden, een cover, en de routes van het contract. Hij voert je tests voor bevindingen 1 tot en met 9 uit tegen je bestanden, en zijn eigen kopie van de aanvalstests uit hoofdstuk 1 tot en met 10. Hij opent nooit een en meet nooit tijd. Niets uit je telt mee.