0%

Kapitel 11 · Abschlussprojekt

Abschlussprojekt: Härte die Leseliste

Abschlussprojekt: Finde jede Lücke mit einem Test

Zehn Kapitel lang hat dir jede Lektion gesagt, wo die Lücke war. Kapitel 3 hat auf die Passworttabelle gezeigt, Kapitel 6 auf die Eigentümerregel, Kapitel 9 auf einen Header. Dieses Kapitel hört damit auf.

Das Projekt in diesem Kapitel ist die Leseliste, so wie du sie nach Kapitel 10 verlassen hast, mit elf wieder eingebauten Schwächen. Nichts markiert sie. Der Code liest sich wie Code, der funktioniert, tests/test_attacks.py enthält fünf Importe und keinen Test, und jede Route antwortet weiterhin. Statt einer Lektion, die zeigt, bekommst du eine Datei: SECURITY.md. Sie beschreibt jede Schwäche so, wie jemand von außen sie bemerken würde, und sie nennt nie eine oder eine Zeile.

Deine Arbeit in dieser Lektion ist, jede dieser elf Beschreibungen in einen Test zu verwandeln, der fehlschlägt. Du schließt noch nichts.

Lies den Vertrag

Öffne SECURITY.md. Die Datei ist eingefroren: Du liest sie und bearbeitest sie nie. Sie hat drei Teile, die heute wichtig sind.

Findings listet elf Symptome auf, jedes mit dem Namen, den sein Test haben muss. Befund 7 lautet „Another reader can delete my book with the form on the page.“, also: Ein anderer Leser kann mein Buch mit dem Formular auf der Seite löschen. Dieser Satz sagt nicht, welche Route, welche Funktion oder welcher fehlende Aufruf. Er sagt, was ein Mensch sehen würde.

What a browser sends ist eine Tabelle für die Befunde 10 und 11. In diesen beiden geht es um Anfragen, die andere Seiten auslösen. Der Vertrag sagt dir außerdem, dass eine Regel von mini_browser.py geschwächt wurde, also kannst du dem Modell bis Lektion 3 nicht trauen. Die Tabelle sagt, was ein korrektes Modell entscheidet, und du darfst diese Anfragen anhand davon von Hand bauen: helpers.form_request() mit einem Header Cookie und einem Header Origin, oder helpers.make_request() mit einem Origin.

Must still work listet auf, was eine Korrektur nicht kaputtmachen darf. Das brauchst du auch heute, denn ein Test darf nichts verlangen, was diese verbietet.

Ein Befund ist ein Symptom. Ein Test ist das genau gemachte Symptom: diese Anfrage, dieser State, diese Zusicherung. Acht der elf Namen sind Tests, die du schon einmal geschrieben hast, in den Kapiteln 2 bis 9. Die anderen drei sind neu, und jeder folgt derselben Form: State aufbauen, Angriff ausführen, zusichern, dass er abgelehnt wurde.

Warum alle elf Tests schreiben, bevor man irgendetwas schließt?

Zwei Befunde, die du in keiner Antwort siehst

Die Befunde 1 und 2 hinterlassen keine Spur in einem Status, einem Header oder einem Body. Eine Anmeldung für einen Namen, den es nicht gibt, bekommt dieselbe 401 wie ein falsches Passwort. Ein Tag, der mit == verglichen wird, gibt dieselbe Antwort wie einer, der sicher verglichen wird. Der Unterschied liegt in der Arbeit, die der Server leistet.

Genau das hast du schon einmal getestet. helpers.count_scrypt() zählt jeden Aufruf von hashlib.scrypt ab dem Moment, in dem du es aufrufst, und hält die Kosten n jedes Aufrufs fest. helpers.count_compare_digest() macht dasselbe für hmac.compare_digest. Der Test aus Kapitel 3 für einen unbekannten Benutzer und der Test aus Kapitel 2 für verify() beruhen beide auf einer Zählung. Nichts wird gemessen, weder hier noch sonst irgendwo in diesem Kurs: Eine Zählung ist exakt, eine Stoppuhr nicht.

Befund 1 ist in keiner Antwort zu sehen. Wie sieht ein Test ihn, ohne Zeit zu messen?

Ein Test, der etwas beweist

Ein roter Test ist nur die Hälfte des Beweises. Derselbe Test muss bestehen, sobald der Befund geschlossen ist, und das heißt, er muss gegen einen korrekten Server bestehen. Zwei Fehler machen das kaputt, in entgegengesetzte Richtungen.

Der erste ist ein Test, der die Lücke nie findet. Nimm Befund 9. Ein naheliegender erster Versuch fragt /covers/..%2Fsecret.txt an und sichert eine 404 zu. Führ ihn aus, und er besteht gegen den Code in diesem Ordner, weil dieser Code einen Namen mit zwei Punkten bereits ablehnt. Der Test ist grün, und die Lücke ist immer noch da. Ein Cover-Name kann den Ordner auf mehr als eine Weise verlassen, und Kapitel 7 hat sie genannt.

Ein Test für Befund 9 fragt ../secret.txt an, bekommt None und besteht gegen den Startcode. Was sagt dir das?

Der zweite Fehler ist ein Test, der zu viel verlangt. Ein Test für Befund 5, der ein Token bearbeitet und es dann mit steps.TEST_KEYS neu signiert, schlägt gegen jeden Server fehl, auch gegen korrekte, weil kein Angreifer diesen Schlüssel hat und ein Server akzeptieren muss, was sein eigener Schlüssel signiert hat. Beginn jeden Test mit seiner Vorbedingung, wie du es seit Kapitel 1 tust: Sichere zuerst zu, dass die ehrliche Anfrage funktioniert, dann führ den Angriff aus.

Vier Befunde brauchen jeweils noch einen Gedanken. Halte die Uhr bei Befund 4 unter dem absoluten Limit, damit nur das Leerlauflimit die beenden kann. Ruf bei Befund 6 signing.load_keys() mit einem auf, das du baust, wie in Kapitel 5. Ruf reading_server.main() nicht aus einem Test auf: Solange der Befund offen ist, startet es einen Server und wartet auf Verbindungen. Sende bei Befund 10 ein vertrauenswürdiges Origin, sonst antwortet die Origin-Prüfung, bevor das Formular-Token überhaupt angesehen wird. Für Befund 7 und Befund 10 brauchst du ein Formular-Token: Es steht in der Session, unter state["sessions"][session_id]["csrf"], und die Session-ID ist der Teil nach = in dem Paar, das steps.login_cookie() zurückgibt.

Deine Aufgabe

Schreib die elf Tests in tests/test_attacks.py, mit den Namen, die SECURITY.md vorgibt. Greif über die auf den Code zu, wie der Kommentar oben in der Datei sagt: reading_server.handle(...), signing.verify(...), passwords.check_login(...). Dann führ sie aus:

python -m pytest tests/test_attacks.py -q

Das Ergebnis, das du in dieser Lektion anstrebst, ist ungewöhnlich:

11 failed

Ein schlichtes python -m pytest -q zeigt 15 failed, weil vier Tests des Modells in tests/test_browser.py ebenfalls rot sind. Die sind für Lektion 3. Lies jeden Fehlschlag, bevor du weitermachst. Ein Test, der an seiner Vorbedingung scheitert oder mit einem in seinem eigenen Aufbau, ist aus dem falschen Grund rot.

Dann drück Check my work. Die Prüfung sieht sich den Code in diesem Ordner überhaupt nicht an. Sie hat eine korrekte Kopie des Dienstes und elf Kopien, die jeweils einen Befund haben, und führt deine Tests gegen sie aus: Jeder Test muss gegen die korrekte bestehen, und jeder muss gegen die Kopie mit seinem Befund fehlschlagen. Der Ordner enthält also immer noch den schwachen Server, wenn diese Lektion fertig ist. Die nächste Lektion setzt dort an.

Aufgabe

Schreib die elf Angriffstests, die SECURITY.md nennt, in tests/test_attacks.py. Ändere in dieser Lektion sonst nichts.

  • Gib jedem Test genau den Namen aus der Tabelle des Vertrags, von test_attack_unknown_user_costs_the_same_work bis test_attack_another_origin_cannot_read_the_list.

  • Jeder Test führt seinen Angriff aus und sichert zu, dass der Angriff abgelehnt wird. Also schlägt jeder gegen den Code in diesem Ordner fehl und würde bestehen, sobald sein Befund geschlossen ist.

  • Sichere zuerst den ehrlichen Fall zu, wo es einen gibt: Die Anmeldung funktioniert, das Token wird akzeptiert, das Cover wird ausgeliefert. Dann greif an.

  • Greif über die auf den Dienst zu: reading_server.handle(...), signing.verify(...), signing.load_keys(...), passwords.check_login(...). Schreib nicht from reading_server import ..., und öffne aus einem Test keinen und starte nicht pl_sec.py.

  • Die Befunde 1 und 2 zeigen sich nur in geleisteter Arbeit. Verwende helpers.count_scrypt() und helpers.count_compare_digest().

  • Bau für die Befunde 10 und 11 die Anfragen von Hand anhand der Tabelle “What a browser sends” in SECURITY.md. Das Formular-Token einer ist state["sessions"][session_id]["csrf"].

  • Jedes POST an /login, /logout, /prefs, /tokens und /books/delete braucht Origin: https://reading.example, also steps.OWN. steps.login_cookie() und steps.bearer_for() senden es für dich.

Führ python -m pytest tests/test_attacks.py -q im aus. Elf Fehlschläge sind das Ergebnis, das du anstrebst.

Dann drück Check my work. Die Prüfung liest deine Testdatei auf die elf Namen und auf Importe, die einen ausgetauschten Server verbergen würden. Sie führt alle elf Tests gegen eine korrekte Kopie des Dienstes aus, wo jeder bestehen muss, und dann jeden Test einzeln gegen eine Kopie, die nur den Befund dieses Tests hat, wo er durch eine Zusicherung fehlschlagen muss. Sie bewertet nicht den Code in diesem Ordner, der noch jede Schwäche enthält, und nichts aus deinem Terminal wird bewertet.