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-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
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
| Schritt | Die Seite ist auf | Sie fragt an | Der Server antwortet | Cookie gesendet | Die Seite |
|---|---|---|---|---|---|
| 0 | reading.example | Anmeldung (ein Fetch mit Basic-Zugangsdaten) | 200 | nein | liest sie; das Cookie wird gespeichert |
| 1 | app.reading.example | ihre eigene Seite, von der zweiten Site | 200 | nein | liest sie |
| 2 | app.reading.example | POST /tokens mit Zugangsdaten | 200 | ja | liest das Token |
| 3 | app.reading.example | GET /books mit dem Token, nach einem Preflight | 200 | nein | liest die |
| 4 | app.reading.example | PUT /books/1 mit dem Token, nach einem Preflight | 200 | nein | liest das Buch |
| 5 | app.reading.example | GET /me mit Zugangsdaten | 200 | ja | liest ana |
| 6 | notes.reading.example | das Löschformular, mit einem geratenen Token | 403 | ja | kann die Antwort eines Formulars nicht lesen |
| 7 | notes.reading.example | POST /tokens mit Zugangsdaten | 403 | ja | darf sie nicht lesen |
| 8 | notes.reading.example | GET /me mit Zugangsdaten | 200 | ja | darf sie nicht lesen |
| 9 | other.example | das Löschformular | 403 | nein | kann die Antwort eines Formulars nicht lesen |
| 10 | other.example | GET /about | 200 | nein | darf sie nicht lesen |
| 11 | other.example | GET /books mit einem Token | nie gesendet; der Preflight wurde abgelehnt | nein | blockiert |
| 12 | keine: ein Client, der kein Browser ist | GET /about mit Origin: https://other.example | 200 | nein | er 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
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 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
Führ
python pl_sec.py two-sitesimaus. Sein Ergebnisblock muss sagen, dass alle 13 Schritte dem Vertrag in SECURITY.mdentsprechen, dassserve()zurückgekehrt ist und dass jeder Thread gestoppt hat.Wenn ein Schritt abweicht, finde den Test in
tests/test_attacks.pyodertests/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 -qaus. 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 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