Eindproject: maak de leeslijst veilig · eindproject
Eindproject: Twee sites op loopback
Het bewijs is klaar. Elf tests slagen, de zes van het model slagen, en elk daarvan draaide in het geheugen: er ging een verzoek-handle() en er kwam een antwoord-dict uit. Deze les voegt geen bewijs toe. Ze voegt een demonstratie toe: dezelfde service, achter de echte adapter, die echte bytes op een
Dat is het waard om één keer te zien, want het geheugen slaat dingen over. In het geheugen is er geen Content-Length om fout te doen, geen preflight die als eigen verzoek reist, geen tweede server en geen serve() die moet stoppen.
De reeks
python pl_sec.py two-sites start twee servers op 127.0.0.1. De ene is je leeslijst, via je make_server() en serve(). De andere is een kleine tweede site die de eigen pagina van de frontend serveert. Daarna speelt het drie soorten pagina’s na via je visit.visit() en je mini_browser.py: de frontend op https://app.reading.example, de buur op https://notes.reading.example, en een pagina op https://other.example. De namen bestaan alleen in het model. Elke byte gaat naar deze computer.
Ana is vanaf stap 0 ingelogd op https://reading.example, en de cookiejar van het model bevat haar
| Stap | De pagina staat op | Ze vraagt om | De server antwoordt | Cookie verstuurd | De pagina |
|---|---|---|---|---|---|
| 0 | reading.example | inloggen (een fetch met Basic-credentials) | 200 | nee | leest het; de cookie wordt opgeslagen |
| 1 | app.reading.example | haar eigen pagina, van de tweede site | 200 | nee | leest het |
| 2 | app.reading.example | POST /tokens met credentials | 200 | ja | leest het token |
| 3 | app.reading.example | GET /books met het token, na een preflight | 200 | nee | leest de |
| 4 | app.reading.example | PUT /books/1 met het token, na een preflight | 200 | nee | leest het boek |
| 5 | app.reading.example | GET /me met credentials | 200 | ja | leest ana |
| 6 | notes.reading.example | het verwijderformulier, met een geraden token | 403 | ja | kan het antwoord op een formulier niet lezen |
| 7 | notes.reading.example | POST /tokens met credentials | 403 | ja | mag het niet lezen |
| 8 | notes.reading.example | GET /me met credentials | 200 | ja | mag het niet lezen |
| 9 | other.example | het verwijderformulier | 403 | nee | kan het antwoord op een formulier niet lezen |
| 10 | other.example | GET /about | 200 | nee | mag het niet lezen |
| 11 | other.example | GET /books met een token | nooit verstuurd; de preflight werd geweigerd | nee | geblokkeerd |
| 12 | geen: een client die geen browser is | GET /about met Origin: https://other.example | 200 | nee | drukt de body af |
De statuskolom is wat pl_sec.py ziet op de plek waar het elk verzoek naar de server brengt. Bij een formulier ziet een pagina die status nooit, en visit() geeft alleen terug wat de pagina mag lezen.
Stap 6 en 9 krijgen dezelfde status om verschillende redenen, en in de cookiekolom zie je het verschil. In stap 9 hield SameSite=Lax de cookie thuis, en weigerde de Origin-controle het verzoek ook nog. In stap 6 ging de cookie mee, omdat de buur op dezelfde site zit, en weigerde de Origin-controle het formulier voordat er naar het token werd gekeken.
In stap 8 antwoordt de server 200 met Ana's naam, op een verzoek dat een pagina op notes.reading.example veroorzaakte. Is dat een gat?
Voer het uit
python pl_sec.py two-sites
Met elke bevinding gedicht en het model hersteld, ziet de run er zo uit:
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
De leeslijst handelde veertien verzoeken af: dertien van het model en het ene van de gewone client. pl_sec.py geeft je server zestien verbindingen, opent en sluit de verbindingen die niemand gebruikte, en wacht daarna tot serve() terugkeert. Daarom kunnen de laatste regels zeggen dat de server gesloten is.
Als een stap afwijkt van het contract, noemt het resultaatblok de stap en de kolom. Herstel niets alleen op basis van de uitvoer. Zoek de test die die stap dekt, in tests/test_attacks.py of tests/test_browser.py, laat hem falen, en werk vanaf daar, zoals in de vorige twee lessen. Een verschil in de cookiekolom ligt aan het model. Een verschil in de status ligt aan de server. Een verschil in wat de pagina mag lezen, kan aan allebei liggen.
Wat de run niet kan laten zien
Hij kan bevinding 10 niet laten zien. In stap 6 draagt het formulier van de buur een geraden token, en de Origin-controle weigert het voordat er naar het token wordt gekeken. De stap zou er precies hetzelfde uitzien als bevinding 10 open was. Alleen je test bewijst die bevinding, door een vertrouwde Origin te sturen met het token van een andere
En hij laat geen browser zien. Er deed in deze cursus op de site geen echte browser mee. De drie “pagina’s” zijn beschrijvingen van verzoeken die worden doorgegeven aan een model dat jij schreef en dat je in dit hoofdstuk moest herstellen, en het model volgt de standaarden alleen zover zijn regels reiken.
Stap 12 krijgt de body die stap 10 niet mocht lezen. Wat zegt dat over CORS?
Afronden
Voer alles nog één keer uit:
python -m pytest -q
Zeventien tests slagen. SECURITY.md is net zo bevroren als het was, en de stand ervan is nu deze: elf bevindingen gedicht, elk met een test die je schreef zonder dat iemand je vertelde waar je moest kijken, en F14 bewust open. Een ondertekend token dat uitlekt, werkt nog steeds tot het verloopt, hooguit vijftien minuten, en om dat te dichten zou je een opgeslagen lijst van ingetrokken tokens nodig hebben, en die heeft deze service niet.
Druk daarna op Check my work. Die kan je serve() stopt na de verbindingen die het kreeg en zijn server sluit. En hij voert de aanvalstests van de eerdere hoofdstukken nog één keer uit.
Opdracht
Draai de afgemaakte service en een tweede site op het
Voer
python pl_sec.py two-sitesuit in de. Het resultaatblok moet zeggen dat alle 13 stappen overeenkomen met het contract in SECURITY.md, datserve()is teruggekeerd en dat elke thread is gestopt.Als een stap afwijkt, zoek dan de test in
tests/test_attacks.pyoftests/test_browser.pydie hem dekt, laat die falen, en herstel de code vanaf daar. Versoepel geen test om hem groen te krijgen.Voer
python -m pytest -quit. Alle zeventien tests slagen.
Als les 2 en 3 af zijn, heeft deze les geen nieuwe code nodig.
Druk daarna op Check my work. Die kan je terminal niet zien. Hij voert dezelfde dertien stappen uit in het geheugen via visit.visit(), je mini_browser.py en je handle(), en vergelijkt de status, de tests/test_browser.py uit, de rijen van “Must still work”, en zijn eigen kopie van de aanvalstests uit hoofdstuk 1 tot en met 10. Hij roept serve() aan met een vervangende server die niets opent en handle_request()-aanroepen telt, en make_server() met http.server vervangen, zodat er geen