Sessions, die halten · Abschlussprojekt
Abschlussprojekt: Projekt: Sessions, die halten
Drei Lektionen haben drei Reparaturen gebracht: eine neue ID bei der Anmeldung, zwei Limits und ein
Die Richtlinie
| Frage | Antwort bei der Leseliste |
|---|---|
| Wann beginnt eine | Wenn ein Besucher eine Einstellung speichert, und bei jeder Anmeldung, immer unter einer neuen ID aus secrets |
| Was enthält sie? | user, page_size, created, last_seen |
| Was bewegt sich? | last_seen, bei jeder Anfrage, die die Session aktiv vorfindet |
| Wann endet sie? | Nach IDLE_LIMIT Sekunden ohne Anfrage, ABSOLUTE_LIMIT Sekunden nach ihrem Start, bei der Abmeldung und bei der Anmeldung, wenn eine neue Session sie ersetzt |
| Was sagt das Cookie? | __Host-session, mit Secure, HttpOnly, SameSite=Lax, Path=/ und ohne Domain |
| Was darf ein Cache behalten? | Nichts über eine Session: Jede Antwort, die ein Cookie setzt, und jede Session-Route sagt no-store |
Jede Zeile bis auf eine ist fertig. Die Lücke steckt in „Wann endet sie?“.
Die Session, zu der niemand zurückkommt
current_session entfernt eine abgelaufene Session, wenn ihre ID benutzt wird. Ein Besucher, der nie zurückkommt, benutzt die ID nie, also bleibt der Eintrag in state["sessions"], solange der Server läuft. Und POST /prefs gibt jedem eine Session, der danach fragt.
sweep_sessions(state) schließt die Lücke. Es liest die Zeit aus state["clock"](), entfernt jede Session, für die session_expired(...) wahr ist, und gibt zurück, wie viele es entfernt hat. start_session ruft es als Erstes auf, also wird der Speicher aufgeräumt, wann immer eine Session beginnt.
Eine Einzelheit entscheidet, wie du es schreibst. Einen Eintrag aus einem for-RuntimeError: dictionary changed size during iteration aus, sobald die Schleife ihren nächsten Schritt macht. Räum also in zwei Durchgängen auf: Sammle zuerst die abgelaufenen IDs in einer
Warum sammelt sweep_sessions die abgelaufenen IDs zuerst in einer Liste und entfernt sie in einer zweiten Schleife?
Meilensteine
1. Auch eine viel benutzte Session endet. Schreib test_attack_busy_session_still_ends(). Das ist der Dieb aus Lektion 3, der alle zehn Minuten eine Anfrage sendet. Melde dich unter einer festen Uhr an, die bei 1000 beginnt. Wiederhole dann: Addiere 600 zu clock["now"], frag GET /me und sichere zu, dass der Benutzer ana ist. Mach weiter, solange der nächste Schritt noch unter dem Limit bleibt:
while clock["now"] + 600 < 1000 + reading_server.ABSOLUTE_LIMIT:
Nach der Schleife ist die Session 28200 Sekunden alt. Addiere 601, was sie eine Sekunde über das Limit bringt, frag noch einmal und sichere zu, dass der Benutzer None ist.
2. Die Abmeldung beendet die Session auf dem Server. Schreib test_attack_logout_ends_the_session_on_the_server(). Melde dich mit steps.login_cookie an, frag GET /me und sichere ana zu, sende POST /logout mit dem Cookie, frag dann GET /me mit demselben Cookie und sichere None zu. Der Angreifer ist jeder, der von vor der Abmeldung eine Kopie des Cookies behalten hat.
Führ python -m pytest -q aus. Es endet mit 13 passed: Beide neuen Tests laufen beim ersten Mal durch. Lektion 3 hat das absolute Limit schon gebaut, und
Dein Abmeldetest läuft beim ersten Ausführen durch. Wofür ist er da?
3. Das Aufräumen. Schreib sweep_sessions(state) und ruf es als erste Zeile von start_session auf. Keiner deiner Tests wechselt die Farbe: python -m pytest -q endet weiterhin mit 13 passed.
4. Sieh der Richtlinie zu. Führ das Szenario für Sessions aus:
python pl_sec.py sessions
Es meldet sich an, lässt auf einer eigenen Uhr 31 Minuten vergehen, fragt noch einmal, meldet sich dann an und ab und probiert das alte Cookie. Die IDs sind hier gekürzt:
--- a session that is left alone ---
ana logs in POST /login -> 200
set-cookie: __Host-session=<id>; HttpOnly; Max-Age=28800; Path=/; SameSite=Lax; Secure
ana asks who she is GET /me -> 200 {"user": "ana", "page_size": 20}
(the clock moves 31 minutes)
the same cookie, 31 minutes later GET /me -> 200 {"user": null, "page_size": 20}
--- a session that is logged out ---
ana logs in again POST /login -> 200
ana logs out POST /logout -> 204
set-cookie: __Host-session=""; HttpOnly; Max-Age=0; Path=/; SameSite=Lax; Secure
the cookie from before the logout GET /me -> 200 {"user": null, "page_size": 20}
--- result ---
after 31 idle minutes the session answered as: nobody
after logout the old cookie answered as: nobody
5. Die Befunde. Prüfe in SECURITY.md, dass die Zeilen F3, F4 und F5 jeweils einen Test nennen und closed sagen.
Was noch offen ist
Das Aufräumen begrenzt, wie lange eine tote Session im Speicher bleibt. Es begrenzt nicht, wie viele aktive Besucher-Sessions ein Fremder in einer halben Stunde öffnen kann: Jedes POST /prefs ohne Cookie erzeugt eine, und keine davon läuft vor 30 Minuten ab. Ein echter Dienst fügt eine Obergrenze hinzu oder erzeugt eine Session erst, wenn er etwas hat, das es POST /prefs langsamer. Dieser Kurs nennt beide Probleme und baut keine der Behebungen.
Klicke dann auf Check my work. Deine Sessions halten jetzt. Die Bearer-Tokens nicht: Sie sind immer noch zwei feste
Aufgabe
Stell die
Schreib in
tests/test_attacks.pytest_attack_busy_session_still_ends()undtest_attack_logout_ends_the_session_on_the_server(), wie die Lektion sie beschreibt. Beide verwendensteps.login_cookie(state, "ana", "correct horse"). Der Test mit der viel benutzten Session läuft unterhelpers.fixed_clock(...), fragt alle 600 SekundenGET /meund sichert jedes Malanazu, solange der nächste Schritt noch unterreading_server.ABSOLUTE_LIMITnach der Anmeldung bleibt, bewegt die Uhr dann auf eine Sekunde nach diesem Limit und sichertNonezu. Der Abmeldetest sichert vor der Abmeldunganaund danachNonezu, mit demselben. Schreib in
reading_server.pysweep_sessions(state): Lies die Zeit ausstate["clock"](), entferne jede Session, für diesession_expired(...)wahr ist, und gib zurück, wie viele du entfernt hast.Ruf
sweep_sessions(state)als erste Zeile vonstart_sessionauf.Führ
python -m pytest -q(13 Tests laufen durch) undpython pl_sec.py sessionsaus.Stell in
SECURITY.mdsicher, dass die Zeilen F3, F4 und F5 jeweils einen Test nennen undclosedsagen.
Klicke dann auf Check my work. Es wiederholt die Prüfungen der Lektionen 2, 3 und 4 mit anderen Uhrzeiten, IDs und Seitengrößen: zuerst die zwei Limits und die Uhr, dann Name und Flags des Cookies, dann die neue ID bei der Anmeldung und no-store. Es ruft sweep_sessions auf einem Speicher mit einer aktiven, einer im Leerlauf befindlichen und einer zu alten Session auf, was 2 zurückgeben und die aktive behalten muss, und ruft start_session auf einem solchen Speicher auf, was ihn aufräumen muss; ein Aufräumen, das ein neues http_adapter, ohne einen set-cookie und cache-control, die zurückkommen. Es führt deine fünf Tests dieses Kapitels über ihre Namen gegen Server aus, die es an die Stelle von reading_server.handle und reading_server.sample_state setzt: Gegen einen korrekten Server müssen alle fünf durchlaufen; jeder muss gegen genau den Fehler fehlschlagen, für den er da ist (eine Anmeldung, die die ID behält, kein Leerlauflimit, kein absolutes Limit, eine Abmeldung, die die Session behält, ein Cookie ohne Secure); und alle fünf müssen gegen einen Server fehlschlagen, der jede Anmeldung ablehnt. Zuletzt liest es die Zeilen F3, F4 und F5 von SECURITY.md. Nichts aus deinem