0%

Sessions, die halten · Übung

Sessions, die enden

Öffne SECURITY.md und lies Zeile F3, die du in Kapitel 1 geschrieben hast: Eine , die jemals kopiert wurde, funktioniert weiter. Das sagt Max-Age=3600, und das bittet einen Client nur, seine Kopie nach einer Stunde zu vergessen. Eine Kopie der ID in einer Datei vergisst nichts, und state["sessions"] behält den Eintrag, solange der Server läuft. Diese Lektion gibt einer Session ein Ende, über das der Server entscheidet.

Zwei Limits für zwei Angreifer

Eine Session kann auf zwei Arten missbraucht werden, und jede braucht ihr eigenes Limit.

Ein Leerlauflimit (idle limit) beendet eine Session, die niemand benutzt. Es deckt einen Laptop ab, der offen in einer Bibliothek liegt, und ein Cookie, das auf einem gemeinsam genutzten Computer zurückgeblieben ist: Nach 30 Minuten ohne Anfrage ist die ID nichts mehr .

Ein absolutes Limit (absolute limit) beendet eine Session, die jemand ständig benutzt. Ein Dieb, der eine ID kopiert hat, kann alle paar Minuten eine Anfrage senden, sodass die Session nie im Leerlauf ist. Nur ihr Alter kann sie beenden: 8 Stunden nach der Anmeldung ist die Session vorbei, egal wie viel los war.

idle limit: more than 30 minutes without a request ends the session

login   request   request  31 minutes pass  request
  |--------|---------|-------------------------X  refused and removed

absolute limit: more than 8 hours after login the session ends,
however busy it is

login   one request every ten minutes ...   8 hours
  |--|--|--|--|--|--|--|--|--|--|--|--|--|--|--X  refused and removed

Die erste Zeile zeigt eine Session mit einer Lücke von 31 Minuten: Die Anfrage nach der Lücke wird abgelehnt. Die zweite zeigt eine Session ganz ohne Lücke: Die erste Anfrage nach der 8-Stunden-Marke wird abgelehnt. Die beiden Zahlen stehen in reading_server.py als IDLE_LIMIT = 1800 und ABSOLUTE_LIMIT = 28800, in Sekunden. Sie sind die Wahl dieses Dienstes, kein Standard. Ein Dienst, der mehr schützt, wählt kürzere Limits.

Ein Dieb kopiert eine Session-ID und sendet alle zehn Minuten eine Anfrage damit. Welches Limit beendet diese Session?

Die Zeit ist ein Argument

Beide Limits vergleichen eine gespeicherte Zeit mit der aktuellen Zeit. Der Server bekommt die Zeit aus state["clock"], einer , die Sekunden als Zahl zurückgibt. sample_state() setzt sie auf time.time, die echte Uhr. Dein Code ruft state["clock"]() auf und ruft nie selbst time.time() auf. Das ist die eingeschleuste Zeit aus Python in der Praxis: Weil die Uhr über state hereinkommt, kann ein Test entscheiden, wie spät es ist.

helpers.fixed_clock(holder) gibt eine Uhrfunktion zurück, die die Zeit aus holder["now"] liest. Der Test behält das und ändert es:

clock = {"now": 1000}
state["clock"] = helpers.fixed_clock(clock)

Nach clock["now"] += 1801 sind für den Server dreißig Minuten und eine Sekunde vergangen. Nichts wartet.

Schreib zuerst den Angriff

Der Angreifer tut hier nichts Raffiniertes. Er benutzt eine ID lange nachdem ihr Besitzer aufgehört hat. Der zweite Schritt in tests/steps.py hält den Test kurz: steps.login_cookie(state, user, password) meldet sich über handle() an und gibt das Cookie-Paar zurück, oder None, wenn die Anmeldung abgelehnt wurde.

Der Test setzt die feste Uhr, meldet sich als ana an und fragt GET /me mit dem Cookie. Er sichert zu, dass der Benutzer ana ist, das ist die Vorbedingung: Die Session funktioniert, solange sie frisch ist. Dann addiert er reading_server.IDLE_LIMIT + 1 zu clock["now"], fragt noch einmal und sichert zu, dass der Benutzer None ist.

Führ python -m pytest -q aus. Der Lauf endet mit 1 failed, 9 passed, und die fehlschlagende Zeile ist wieder AssertionError: assert 'ana' is None: Eine halbe Stunde später antwortet die Session immer noch.

Eine reine Frage

Eine Session braucht zwei Zeitstempel, um für sich selbst Auskunft zu geben. start_session liest einmal now = state["clock"]() und speichert es sowohl unter created als auch unter last_seen.

session_expired(session, now) beantwortet dann eine Frage mit zwei Subtraktionen und zwei Vergleichen. now - session["last_seen"] ist, wie lange die Session schon im Leerlauf ist, und now - session["created"] ist, wie alt sie ist. Die Funktion gibt True zurück, wenn der erste Wert größer als IDLE_LIMIT ist oder der zweite größer als ABSOLUTE_LIMIT. Sie liest keine Uhr und ändert nichts, also kannst du sie mit beliebigen Zahlen ausprobieren.

Was gibt das aus?

IDLE_LIMIT = 1800
ABSOLUTE_LIMIT = 28800
session = {"created": 1000, "last_seen": 29000}
now = 30000
idle = now - session["last_seen"] > IDLE_LIMIT
too_old = now - session["created"] > ABSOLUTE_LIMIT
print(idle, too_old)

Ablehnen, entfernen und ein Feld bewegen

In current_session(request, state) greifen die Limits, denn /me, /prefs und session_user gehen alle durch diese Funktion. Kein Aufrufer kann die Limits umgehen. Nachdem sie die Session gefunden hat, liest sie die Zeit und fragt session_expired:

  • Eine abgelaufene Session wird aus state["sessions"] entfernt, und die Funktion gibt None zurück, genau so, als ob das Cookie nichts genannt hätte. Das Entfernen zählt. Eine Session, die abgelehnt und behalten wird, steht immer noch in jeder Kopie des Zustands, und eine geänderte Zeile könnte sie zurückbringen.

  • Eine aktive Session bekommt last_seen = now, und die Funktion gibt sie zurück.

created bewegt sich nie. Wenn eine Anfrage es bewegen würde, sähe die Session nach jeder Anfrage neu aus, und das absolute Limit käme nie.

Das Cookie sagt Max-Age=3600. Ein Angreifer hat die ID in einer Datei gespeichert. Zwei Stunden später, ohne dass jemand die Session benutzt hat: Was hält die ID davon ab zu funktionieren?

Ein weiteres Ereignis sollte Sessions beenden: Wenn sich ein Passwort ändert, sollte jede Session dieses Benutzers enden. Dieser Dienst hat keine Route, die ein Passwort ändert, also gibt es hier nichts zu bauen.

Deine Aufgabe

  1. Schreib in tests/test_attacks.py test_attack_idle_session_is_refused() und sieh zu, wie er fehlschlägt.

  2. Speichere in start_session die Zeit unter created und last_seen.

  3. Schreib session_expired(session, now).

  4. Lehne in current_session eine abgelaufene Session ab und entferne sie, und bewege last_seen bei einer aktiven.

  5. Nenne in Zeile F3 von SECURITY.md deinen Test und setz den Status auf closed in 4.3.

Führ python -m pytest -q noch einmal aus. Es endet mit 10 passed: der Leerlauftest, der Fixierungstest aus Lektion 2 und die acht aus früheren Kapiteln.

Klicke dann auf Check my work. Sessions enden jetzt auf dem Server. Das Cookie, das die ID trägt, sagt immer noch nur HttpOnly, Max-Age und Path, und die nächste Lektion gibt ihm den Rest seiner Anweisungen.

Aufgabe

Schreib den Angriff mit einer im Leerlauf als Test und gib dann jeder Session zwei Limits.

  1. Schreib in tests/test_attacks.py test_attack_idle_session_is_refused(). Verwende clock = {"now": 1000} und state["clock"] = helpers.fixed_clock(clock). Melde dich mit steps.login_cookie(state, "ana", "correct horse") an. Frag GET /me mit dem und sichere zu, dass der Benutzer ana ist. Addiere reading_server.IDLE_LIMIT + 1 zu clock["now"], frag noch einmal und sichere zu, dass der Benutzer None ist. Führ python -m pytest -q aus: Dieser Test muss zuerst fehlschlagen.

  2. Lies in start_session now = state["clock"]() und speichere es sowohl unter created als auch unter last_seen.

  3. Schreib session_expired(session, now): True, wenn now - session["last_seen"] größer als IDLE_LIMIT ist oder now - session["created"] größer als ABSOLUTE_LIMIT.

  4. In current_session, nachdem die Session gefunden ist: Lies die Zeit aus state["clock"](); wenn die Session abgelaufen ist, entferne sie aus state["sessions"] und gib None zurück; andernfalls setz ihr last_seen auf die aktuelle Zeit und gib sie zurück.

  5. Nenne in Zeile F3 von SECURITY.md deinen Test und setz den Status auf closed in 4.3.

Führ im python -m pytest -q aus, bis es mit 10 passed endet.

Klicke dann auf Check my work. Es ruft session_expired mit eigenen Sessions auf, je eine Sekunde innerhalb und eine Sekunde außerhalb jedes Limits, also bestehen sowohl > als auch >=. Unter einer Uhr, die es bewegt, weit weg von der echten Zeit, ruft es start_session auf und liest die beiden Zeitstempel, dann fragt es GET /me über handle(): 100 Sekunden später muss die Session antworten, und nur last_seen darf sich bewegt haben; nach IDLE_LIMIT muss sie null antworten und aus state["sessions"] verschwunden sein; und eine Session, die jeweils kurz vor jedem Leerlauflimit erneut gefragt wird, muss enden, sobald ABSOLUTE_LIMIT vergangen ist. Es führt deinen Test dreimal aus, jedes Mal gegen einen Server, den es an die Stelle von reading_server.handle und reading_server.sample_state setzt: einen, der Sessions im Leerlauf beendet, wo dein Test durchlaufen muss; einen, der das nie tut, wo er fehlschlagen muss; und einen, der jede Anmeldung ablehnt, wo er fehlschlagen muss. Zuletzt liest es Zeile F3 von SECURITY.md und sucht den Namen deines Tests und einen Status, der mit closed beginnt. Nichts aus deinem Terminal wird bewertet.