Wer was darf, und wie oft · Abschlussprojekt
Abschlussprojekt: Projekt: deine Liste und die von niemand anderem
Eigentum, ein Anmeldelimit und ein Log funktionieren jeweils, und jedes hat einen Test. Dieses Projekt formuliert sie als eine Richtlinie und schließt die letzte Lücke darin: was passiert, wenn dein eigener Code scheitert.
Die Richtlinie
| Route | Wer sie aufrufen darf | So sieht eine Ablehnung aus | Festgehalten als |
|---|---|---|---|
GET, HEAD /about | jeder | 405 für eine andere | - |
GET, HEAD /books, /books/{id} | ein Token, das die Prüfung besteht; nur die eigenen Bücher des Aufrufers | 401 ohne Token; 404 für ein Buch, das fehlt oder einem anderen gehört | der Benutzer des Tokens |
POST /books, PUT und DELETE /books/{id} | ein Token, dessen Benutzer schreiben darf; nur die eigenen Bücher des Aufrufers | 401; 403 für ein Konto, das nicht schreiben darf; 404 für ein Buch, das nicht dem Aufrufer gehört | der Benutzer des Tokens |
POST /login | Basic-Zugangsdaten | 401; 429 mit Retry-After, wenn der Name oder der ganze Dienst über seinem Limit ist | der Benutzer nach einem korrekten Passwort, sonst - |
POST /tokens | eine | 401 | der Benutzer der Session |
GET /me, POST /prefs | jeder | keine | der Benutzer der Session, wenn die Anfrage eine aktive Session liest, sonst - |
POST /logout | jeder | keine | - |
Jede Anfrage, die handle() erreicht, wird einmal aufgeschrieben, in fünf Feldern und ohne Berechtigungsnachweis. Die Lesezugriffe auf Bücher antworten mit private, no-cache, und die fünf Session-Routen antworten mit no-store.
Wenn dein eigener Code scheitert
Die Tabelle sagt, was der Server mit Anfragen tut, die er versteht. Ein Fall fehlt: ein Fehler im Server selbst. Heute entkommt ein Fehler in route() aus handle(). Der Client bekommt von deinem Code keine Antwort, und das Log bekommt keine Zeile.
handle() ist die eine Stelle, die jede Antwort passiert, die deine Routen bauen, also gehört die Absicherung dorthin. (Die 411, 413 und 400, mit denen read_body antwortet, bevor eine Route läuft, passieren sie nicht und werden nicht festgehalten.) Pack den Aufruf von route in try. Bau in except Exception: die Antwort:
traceback.print_exc()gibt denauf der Standardfehlerausgabe aus, wo der Betreiber liest. Das tracebackist neu, das Projekt importiert es bereits, und dieser eine Aufruf ist alles, was du davon brauchst.json_error(500, ...)mit einer festen Meldung deiner Wahl.cache-controlaufno-storegesetzt.Ein leerer Body, wenn die Methode
HEADist. Das ist die Regel auswith Python, die routedieses Mal nicht anwenden konnte.
Behalte die Log-Zeile nach dem try, damit auch die gescheiterte Anfrage festgehalten wird, als 500. Lektion 5 hat log_line so geschrieben, dass es nicht scheitern kann, also kann nach dem try nichts einen Fehler auslösen.
Warum Exception und kein nacktes except? Ctrl+C löst KeyboardInterrupt aus, und so hältst du einen Server über die Tastatur an. KeyboardInterrupt ist keine Exception, also lässt diese Absicherung es durch, und der Server hält trotzdem an.
Python in der Praxis hat dir beigebracht, eng abzufangen, und das gilt überall sonst weiterhin. Das ist die eine Stelle, an der ein weites Abfangen die richtige Grenze ist, und es ist die äußerste: Was auch immer in route() schiefgegangen ist, der Client bekommt eine Antwort und der Betreiber die Einzelheiten.
Der Stream des Betreibers ist der richtige Ort für einen Traceback. Er ist kein sicherer Ort. Ein Traceback kann einen
Warum fängt die Absicherung Exception ab und nicht jede mögliche Ausnahme?
Meilensteine
1. Der Angriffstest. Ein Client kann deinen Server schon jetzt einen Fehler auslösen lassen. Sende GET /books/ gefolgt von 5.000 Ziffern: int() lehnt eine so lange Zahl mit einem handle(), und der Client bekommt gar keine Antwort. Eine Absicherung ist für diesen Fehler da und für die, die noch niemand gefunden hat. Der Test verwendet einen Fehlschlag, der schneller aufzuschreiben ist: Er macht den Zustand von Hand kaputt. Mit steps.keyed_state() und einem Token für ana entfernt er die Bücher mit state.pop("books") und sendet GET /books. Er sichert den Status 500 zu und dass weder b"Traceback" noch b"KeyError" im Body steht. Führ python -m pytest -q aus: 1 failed, 20 passed. Der KeyError entkommt aus handle() und lässt den Test fehlschlagen.
2. Die Absicherung. Schreib sie in handle, wie oben beschrieben.
3. Alles zusammen. python -m pytest -q endet mit 21 passed. Führ dann beide Szenarien aus:
python pl_sec.py other-reader
python pl_sec.py guessing
Jedes endet mit dem Log deines Servers. Im ersten kannst du bens Versuch lesen:
1000000 DELETE /books/1 404 ben
4. Die Befunde. In SECURITY.md nennen die Zeilen F8 und F9 jeweils ihren Test und sagen closed.
bens abgelehntes Löschen steht im Log als ... DELETE /books/1 404 ben. Was gibt diese Zeile einem Betreiber, das die 404 allein nicht gegeben hat?
Was noch offen ist
F14 aus Kapitel 5 ist noch offen: Ein Token, das geleakt wird, funktioniert, bis es abläuft.
Und sieh dir die Bücher noch einmal an. Ein Titel kann jeder Text sein, den ein Client sendet, bis zur Grenze des Bodys. Dein Server speichert ihn und gibt ihn ohne zweiten Blick zurück. Das nächste Kapitel gibt einem Buch drei weitere Felder und fragt, was passiert, wenn Text aus einer Anfrage als mehr als nur Text gelesen wird.
Aufgabe
Schließ die letzte Lücke: Ein Fehler in deinem eigenen Code muss zu einer festen 500 werden.
Schreib in
tests/test_attacks.pytest_attack_error_body_shows_no_traceback(). Entferne mitstate = steps.keyed_state()undbearer = steps.bearer_for(state, "ana", "correct horse")die Bücher mitstate.pop("books"), sendeGET /booksmit dem Token im HeaderAuthorizationund sichere zu, dass der Status 500 ist und dass wederb"Traceback"nochb"KeyError"im Body steht. Führpython -m pytest -qaus: Dieser Test muss zuerst fehlschlagen.Setz in
handleinreading_server.pyden Aufruf vonrouteintry. Ruf inexcept Exception:traceback.print_exc()auf, baujson_error(500, ...)mit einer festen Meldung, setz seincache-controlaufno-storeund leere seinen Body, wenn dieHEADist. Behalte die Log-Zeile nach demtry, damit sie auch für eine 500 geschrieben wird.Führ
python -m pytest -qaus, bis es mit21 passedendet, und führ dannpython pl_sec.py other-readerundpython pl_sec.py guessingaus.Stell in
SECURITY.mdsicher, dass die Zeilen F8 und F9 jeweils einen Test nennen undclosedsagen.
Klicke dann auf Check my work. Es setzt eine reading_server.route und ruft reading_server.handle(...) auf: Die Antwort muss Status 500 mit einem festen no-store, ein leerer Body für HEAD, eine Log-Zeile mit Status 500 und ein KeyboardInterrupt muss durchgelassen werden. Es wiederholt die Prüfungen der Lektionen 2 bis 5 mit neuen Benutzern, Büchern und Zeiten. Es führt die vier Tests dieses Kapitels, ausgewählt über ihre Namen, gegen Server aus, die es an die Stelle von reading_server.handle und reading_server.sample_state setzt: Gegen einen korrekten müssen alle vier durchlaufen, jeder muss gegen einen Server mit seinem eigenen Fehler fehlschlagen, und alle müssen gegen einen fehlschlagen, der jede Anfrage ablehnt. Es liest die Zeilen F8 und F9 von SECURITY.md. Nichts aus deinem