0%

Abschlussprojekt: Härte die Leseliste · Abschlussprojekt

Abschlussprojekt: Schließ die Lücken auf dem Server

Du hast elf rote Tests und einen Server, dessen Schwächen du im Code noch nicht gesehen hast. Diese Lektion schließt die ersten neun. Sie alle liegen beim Server: in reading_server.py, signing.py und passwords.py. In den Befunden 10 und 11 geht es um Anfragen von anderen Seiten, und sie warten auf die nächste Lektion.

Die ist die Regressionsschleife aus Python in der Praxis, ein Befund nach dem anderen:

  1. Führ einen Test aus und lies seinen Fehlschlag.

  2. Finde den Code, auf den der Fehlschlag zeigt.

  3. Ändere ihn.

  4. Führ alle Tests aus, um zu sehen, dass der gewünschte grün geworden ist und nichts anderes rot.

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

-k wählt die Tests aus, deren Namen diesen Text enthalten. Ohne es laufen alle elf.

Von einem Symptom zu einer Zeile

Hier ist der ganze Weg einmal, für Befund 7. Die anderen acht sind deine.

Das Symptom ist „Another reader can delete my book with the form on the page.“ Dein Test meldet Ben an, sendet das Formular für Buch 1 mit Bens eigenem und Formular-Token und sichert eine 404 zu. Er schlägt fehl:

E       assert 303 == 404

Eine 303 ist die Antwort auf ein Löschen, das funktioniert hat. Die Anfrage hat also jede Prüfung auf ihrem Weg bestanden: die Methode, das Origin, die , das Formular, das Formular-Token und die ID. Der Vertrag nennt diese Reihenfolge in seinem Abschnitt über die Routen, und die nächste Prüfung darin ist der Eigentümer.

Das Formular sendet an /books/delete. In route() findet dieser Zweig die Session und ruft delete_from_form() auf. Lies diese gegen die Regel aus Kapitel 6: Eine Anfrage wird abgelehnt, außer eine Regel erlaubt sie. Die Funktion schlägt das Buch nach, prüft, dass die ID existiert, und entfernt es. Sie fragt nie, wem das Buch gehört. DELETE /books/1 mit einem Token fragt may_change() über item_refusal(). Das Formular ist ein zweiter Weg zum selben Löschen, und es hat die Frage übersprungen. Das war die Lehre aus Kapitel 6: Eine Eigentümerregel, die auf einem Weg geprüft wird und auf einem anderen nicht, ist keine Regel.

Die Korrektur ist eine , und die Funktion, die aufzurufen ist, existiert schon.

Achte darauf, was die Zeile gefunden hat. Es war keine Suche nach „Bug“. Es waren der Fehlschlag des Tests, die Reihenfolge der Prüfungen im Vertrag und eine Regel, die du schon kennst. Jeder der anderen Befunde gibt denselben drei Dingen nach. Die Regel steht in dem Kapitel, das den Befund zuerst geschlossen hat, und die Funktion, die du damals geschrieben hast, steht meist noch in der Datei, nur nicht mehr aufgerufen oder nicht mehr vollständig.

Schließ die Tür nicht für alle

Ein Befund ist geschlossen, wenn sein Angriff abgelehnt wird und jede ehrliche Anfrage weiterhin funktioniert. Die zweite Hälfte geht leicht verloren. Nimm Befund 9. Diese Änderung lässt seinen Test sofort bestehen:

def safe_cover_path(folder, name):
    return None

Kein Name kann jetzt den Ordner verlassen. Es wird aber auch kein Cover ausgeliefert, und SECURITY.md sagt unter “Must still work”, dass GET /covers/kindred.txt das Cover zurückgibt. Check my work liest auch diese . Die Prüfung meldet Leser an und zählt die Passwort-Hashes, hält eine Session am Leben, indem sie sie jede halbe Stunde benutzt, löscht das eigene Buch eines Lesers mit seinem eigenen Formular-Token, fügt ein Buch mit allen fünf Feldern hinzu und fragt ein Cover an. Eine Korrektur, die eins davon ablehnt, wird genauso deutlich gemeldet wie ein offener Befund.

Eine Änderung lässt safe_cover_path() für jeden Namen None zurückgeben. Der Test für Befund 9 besteht. Ist der Befund geschlossen?

Dieselbe Sorgfalt gilt für den Status, den eine Ablehnung bekommt. Der Vertrag antwortet 404 für ein Buch, das nicht dem Aufrufer gehört, damit die Antwort die ID nicht bestätigt, und 422 für einen Body, der ein Feld nennt, das der Client nicht setzen darf. Eine Korrektur, die 403 antwortet oder das Feld stillschweigend weglässt, schließt die Lücke und bricht den Vertrag.

Deine Aufgabe

Schließ die Befunde 1 bis 9 in der Reihenfolge des Vertrags. Führ nach jedem alle Tests aus:

python -m pytest tests/test_attacks.py -q

Die Zahl der Fehlschläge sollte jedes Mal um eins sinken:

Nach BefundErwartete Fehlschläge
110
29
38
47
56
65
74
83
92

Wenn die Zahl um zwei sinkt, lies den Diff noch einmal: Eine Änderung hat etwas geschlossen, das du nicht anfassen wolltest, und du willst wissen, warum. Wenn sie nicht sinkt, ist die Fehlermeldung dieses einen Tests die Stelle, an der du suchen solltest.

Zwei Hinweise dazu, wo was liegt. Die Befunde 1 und 2 sind nicht in reading_server.py: Die Arbeit einer Anmeldung passiert in passwords.py, und ein Tag wird in signing.py verglichen. Die Befunde 5 und 6 sind ebenfalls in signing.py. Alles andere ist in reading_server.py.

Nach dieser Lektion schlagen noch zwei Tests fehl. Welche, und warum ist das erwartet?

Dann drück Check my work. Die Prüfung ruft deine Funktionen und handle() mit Benutzern, Büchern, einem Schlüssel und Uhrzeiten auf, die du nicht gesehen hast, einmal für jeden der neun Befunde, und sie sieht sich nicht an, wie eine Korrektur geschrieben ist, nur, was der Code tut. Sie prüft jede Zeile von “Must still work”. Sie führt deine eigenen Tests für die Befunde 1 bis 9 gegen deine Dateien aus. Und sie führt ihre eigene Kopie der Angriffstests aus, die du in den Kapiteln 1 bis 10 geschrieben hast, weil die Testdatei in diesem Kapitel leer begonnen hat und sonst nichts diese früheren Zeilen schützt.

Aufgabe

Schließ die Befunde 1 bis 9 von SECURITY.md in reading_server.py, signing.py und passwords.py, ohne etwas unter “Must still work” kaputtzumachen.

Nach deinen Änderungen gilt:

  1. Eine Anmeldung für einen Namen, den es nicht gibt, kostet einen Aufruf von hashlib.scrypt zu den üblichen Kosten, wie ein falsches Passwort, und bekommt dieselbe Antwort.

  2. signing.verify() entscheidet über hmac.compare_digest.

  3. Eine Anmeldung bekommt immer eine neue . Die Session, die der Besucher hatte, wird entfernt, und ihre Seitengröße bleibt erhalten.

  4. Eine Session, die länger als IDLE_LIMIT nicht benutzt wurde, wird abgelehnt und entfernt. Eine Session, die benutzt wird, hält weiterhin bis ABSOLUTE_LIMIT.

  5. Ein Token wird abgelehnt, wenn irgendein Teil seiner Nutzdaten bearbeitet wurde, exp eingeschlossen. Ein Token, das der Server erstellt hat, wird bis exp akzeptiert.

  6. signing.load_keys() löst aus, wenn READING_KEY fehlt, leer ist oder kürzer als 32 Bytes, und main() gibt dann eine Zeile auf der Standardfehlerausgabe aus und gibt 1 zurück.

  7. Das Löschformular antwortet 404 für ein Buch, das nicht dem Aufrufer gehört, und das Buch bleibt.

  8. Ein Buch-Body, der owner oder id nennt, wird mit 422 beantwortet, mit dem Schlüssel in der Meldung, und nichts wird gespeichert.

  9. safe_cover_path() gibt nur für eine Datei innerhalb des aufgelösten Cover-Ordners einen Pfad zurück.

Lass die Befunde 10 und 11 und mini_browser.py für die nächste Lektion.

Führ nach jedem Befund python -m pytest tests/test_attacks.py -q aus. Die Fehlschläge gehen von 11 auf 2 zurück.

Dann drück Check my work. Die Prüfung prüft jeden der neun Befunde über handle() und deine , unter einem eigenen State mit Benutzern, Büchern, einem Schlüssel und Uhrzeiten, die du nicht gesehen hast, und zählt Aufrufe von hashlib.scrypt und hmac.compare_digest, wo es bei einem Befund um geleistete Arbeit geht. Sie prüft die Zeilen von “Must still work”: ein Passwort-Hash für eine korrekte Anmeldung, eine Session, die am Leben bleibt, solange sie benutzt wird, ein Token unter dem vorherigen Schlüssel, das eigene Löschen eines Lesers, ein Buch mit allen fünf Feldern, ein Cover und die Routen des Vertrags. Sie führt deine Tests für die Befunde 1 bis 9 gegen deine Dateien aus und ihre eigene Kopie der Angriffstests aus den Kapiteln 1 bis 10. Sie öffnet nie einen und misst nie Zeit. Nichts aus deinem wird bewertet.