0%

Abschlussprojekt: Härte die Leseliste · Abschlussprojekt

Abschlussprojekt: Zwei Sites auf Loopback

Der Beweis ist erbracht. Elf Tests bestehen, die sechs des Modells bestehen, und jeder davon lief im Speicher: Ein Anfrage- ging in handle() hinein, und ein Antwort-Dictionary kam heraus. Diese Lektion fügt keinen Beweis hinzu. Sie fügt eine Vorführung hinzu: derselbe Dienst, hinter dem echten Adapter, der echte Bytes auf einem beantwortet, mit einer zweiten Site daneben.

Das lohnt sich einmal anzusehen, denn im Speicher fällt manches weg. Im Speicher gibt es kein Content-Length, das man falsch setzen kann, keinen Preflight, der als eigene Anfrage reist, keinen zweiten Server und kein serve(), das aufhören muss.

Die Abfolge

python pl_sec.py two-sites startet zwei Server auf 127.0.0.1. Einer ist deine Leseliste, über dein make_server() und serve(). Der andere ist eine kleine zweite Site, die die eigene Seite des Frontends ausliefert. Dann spielt es drei Arten von Seiten durch dein visit.visit() und dein mini_browser.py: das Frontend auf https://app.reading.example, den Nachbarn auf https://notes.reading.example und eine Seite auf https://other.example. Die Namen existieren nur im Modell. Jedes Byte geht an diesen Computer.

Ana ist ab Schritt 0 auf https://reading.example angemeldet, und der -Speicher des Modells enthält ihr Session-Cookie. Bevor du irgendetwas ausführst, deck die letzten drei Spalten ab und sag sie vorher.

SchrittDie Seite ist aufSie fragt anDer Server antwortetCookie gesendetDie Seite
0reading.exampleAnmeldung (ein Fetch mit Basic-Zugangsdaten)200neinliest sie; das Cookie wird gespeichert
1app.reading.exampleihre eigene Seite, von der zweiten Site200neinliest sie
2app.reading.examplePOST /tokens mit Zugangsdaten200jaliest das Token
3app.reading.exampleGET /books mit dem Token, nach einem Preflight200neinliest die
4app.reading.examplePUT /books/1 mit dem Token, nach einem Preflight200neinliest das Buch
5app.reading.exampleGET /me mit Zugangsdaten200jaliest ana
6notes.reading.exampledas Löschformular, mit einem geratenen Token403jakann die Antwort eines Formulars nicht lesen
7notes.reading.examplePOST /tokens mit Zugangsdaten403jadarf sie nicht lesen
8notes.reading.exampleGET /me mit Zugangsdaten200jadarf sie nicht lesen
9other.exampledas Löschformular403neinkann die Antwort eines Formulars nicht lesen
10other.exampleGET /about200neindarf sie nicht lesen
11other.exampleGET /books mit einem Tokennie gesendet; der Preflight wurde abgelehntneinblockiert
12keine: ein Client, der kein Browser istGET /about mit Origin: https://other.example200neiner gibt den Body aus

Die Statusspalte ist das, was pl_sec.py sieht, wo es jede Anfrage zum Server trägt. Eine Seite sieht ihn bei einem Formular nie, und visit() gibt nur zurück, was die Seite lesen darf.

Die Schritte 6 und 9 bekommen denselben Status aus verschiedenen Gründen, und in der Cookie-Spalte unterscheiden sie sich. In Schritt 9 hat SameSite=Lax das Cookie zu Hause gehalten, und die Origin-Prüfung hat die Anfrage zusätzlich abgelehnt. In Schritt 6 ging das Cookie mit, weil der Nachbar auf derselben Site ist, und die Origin-Prüfung hat das Formular abgelehnt, bevor das Token angesehen wurde.

In Schritt 8 antwortet der Server mit 200 und Anas Namen auf eine Anfrage, die eine Seite auf notes.reading.example ausgelöst hat. Ist das eine Lücke?

Führ es aus

python pl_sec.py two-sites

Mit allen Befunden geschlossen und dem reparierten Modell sieht der Lauf so aus:

pl_sec: two-sites, your server and a second site on 127.0.0.1 (a port the system chose)
The names exist only in the model. Every byte goes to this computer, over plain HTTP.
--- ana logs in at the reading list ---
step  0  reading.example        log in                                 -> 200        cookie: no   reads the answer
--- the front end ---
step  1  app.reading.example    its own page, from the second site     -> 200        cookie: no   reads the answer
step  2  app.reading.example    POST /tokens with credentials          -> 200        cookie: yes  reads the answer
step  3  app.reading.example    GET /books with the token              -> 200        cookie: no   reads the answer
step  4  app.reading.example    PUT /books/1 with the token            -> 200        cookie: no   reads the answer
step  5  app.reading.example    GET /me with credentials               -> 200        cookie: yes  reads the answer
--- the neighbor on the same site ---
step  6  notes.reading.example  the delete form, with a guessed token  -> 403        cookie: yes  does not read the answer
step  7  notes.reading.example  POST /tokens with credentials          -> 403        cookie: yes  does not read the answer
step  8  notes.reading.example  GET /me with credentials               -> 200        cookie: yes  does not read the answer
--- a page on another site ---
step  9  other.example          the delete form                        -> 403        cookie: no   does not read the answer
step 10  other.example          GET /about                             -> 200        cookie: no   does not read the answer
step 11  other.example          GET /books with a token                -> never sent cookie: no   does not read the answer
--- a client that is not a browser ---
step 12  GET /about with Origin: https://other.example  -> 200  it prints the body: {"service": "reading list", "version": 1}
--- result ---
book 1 is still in the list, with status 'done'
all 13 steps match the contract in SECURITY.md
serve() returned after 16 connections; the server is closed
pl_sec: every thread has stopped

Die Leseliste hat vierzehn Anfragen bearbeitet: dreizehn vom Modell und die eine des einfachen Clients. pl_sec.py gesteht deinem Server sechzehn Verbindungen zu, öffnet und schließt die, die niemand benutzt hat, und wartet dann, bis serve() zurückkehrt, und deshalb können die letzten Zeilen sagen, dass der Server geschlossen ist.

Wenn ein Schritt vom Vertrag abweicht, nennt der Ergebnisblock den Schritt und die Spalte. Repariere nichts allein anhand des Protokolls. Finde den Test, der diesen Schritt abdeckt, in tests/test_attacks.py oder tests/test_browser.py, bring ihn zum Fehlschlagen und arbeite von dort aus, wie in den letzten beiden Lektionen. Ein Unterschied in der Cookie-Spalte gehört zum Modell. Ein Unterschied im Status gehört zum Server. Ein Unterschied darin, was die Seite lesen darf, kann zu beiden gehören.

Was der Lauf nicht zeigen kann

Er kann Befund 10 nicht zeigen. In Schritt 6 trägt das Formular des Nachbarn ein geratenes Token, und die Origin-Prüfung lehnt es ab, bevor das Token angesehen wird. Der Schritt würde mit offenem Befund 10 genau gleich aussehen. Nur dein Test beweist diesen Befund, indem er ein vertrauenswürdiges Origin mit dem Token einer anderen sendet.

Und er zeigt keinen Browser. In diesem Kurs hat auf der Site kein echter Browser mitgewirkt. Die drei „Seiten“ sind Beschreibungen von Anfragen, die einem Modell übergeben werden, das du geschrieben hast und das du in diesem Kapitel reparieren musstest, und das Modell folgt den Standards nur so weit, wie seine Regeln reichen.

Schritt 12 bekommt den Body, den Schritt 10 nicht lesen durfte. Was sagt das über CORS?

Abschluss

Führ ein letztes Mal alles aus:

python -m pytest -q

Siebzehn Tests bestehen. SECURITY.md ist so eingefroren wie zuvor, und ihr Stand ist jetzt dieser: elf Befunde geschlossen, jeder mit einem Test, den du geschrieben hast, ohne dass dir jemand gesagt hat, wo du suchen sollst, und F14 absichtlich offen. Ein signiertes Token, das durchsickert, funktioniert weiterhin, bis es abläuft, höchstens fünfzehn Minuten, und um das zu schließen, bräuchte es eine gespeicherte Liste zurückgezogener Tokens, die dieser Dienst nicht hat.

Dann drück Check my work. Die Prüfung kann dein nicht sehen, also gehört der -Lauf dir allein. Sie führt dieselben dreizehn Schritte im Speicher aus, mit deinem Server und deinem Modell, und vergleicht jede Zeile mit dem Vertrag; wenn eine Zeile abweicht, setzt sie ein korrektes Modell und dann einen korrekten Server ein, um dir zu sagen, ob du beim Modell oder beim Server suchen sollst. Sie führt deine elf Tests gegen deine Dateien aus und dann jeden noch einmal gegen einen Server, der seinen Befund hat, um zu sehen, dass kein Test auf dem Weg zu Grün gelockert wurde. Sie prüft, dass serve() nach den Verbindungen aufhört, die es bekommen hat, und seinen Server schließt, mit einem Platzhalter, der nichts öffnet. Und sie führt die Angriffstests der früheren Kapitel noch ein letztes Mal aus.

Aufgabe

Lass den fertigen Dienst und eine zweite Site auf der laufen und repariere alles, was der Lauf zeigt.

  • Führ python pl_sec.py two-sites im aus. Sein Ergebnisblock muss sagen, dass alle 13 Schritte dem Vertrag in SECURITY.md entsprechen, dass serve() zurückgekehrt ist und dass jeder Thread gestoppt hat.

  • Wenn ein Schritt abweicht, finde den Test in tests/test_attacks.py oder tests/test_browser.py, der ihn abdeckt, bring ihn zum Fehlschlagen und repariere den Code von dort aus. Lockere keinen Test, um ihn grün zu bekommen.

  • Führ python -m pytest -q aus. Alle siebzehn Tests bestehen.

Wenn die Lektionen 2 und 3 vollständig sind, braucht diese Lektion keinen neuen Code.

Dann drück Check my work. Die Prüfung kann dein Terminal nicht sehen. Sie führt dieselben dreizehn Schritte im Speicher über visit.visit(), dein mini_browser.py und dein handle() aus und vergleicht den Status, das und das, was die Seite lesen darf, mit dem Vertrag; wenn eine Zeile abweicht, probiert sie nacheinander ein korrektes Modell und einen korrekten Server aus, um zu sagen, ob du beim Modell oder beim Server suchen sollst. Sie führt deine elf Tests gegen deine Dateien aus und jeden noch einmal gegen einen Server, der seinen Befund hat, wo er weiterhin fehlschlagen muss. Sie führt tests/test_browser.py aus, die Zeilen von “Must still work” und ihre eigene Kopie der Angriffstests aus den Kapiteln 1 bis 10. Sie ruft serve() mit einem Platzhalter-Server auf, der nichts öffnet und die Aufrufe von handle_request() zählt, und make_server() mit ersetztem http.server, sodass kein gebunden wird.