Sessions, die halten · Übung
Sessions, die enden
Öffne SECURITY.md und lies Zeile F3, die du in Kapitel 1 geschrieben hast: Eine 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 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
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 gibtNonezurü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
Schreib in
tests/test_attacks.pytest_attack_idle_session_is_refused()und sieh zu, wie er fehlschlägt.Speichere in
start_sessiondie Zeit untercreatedundlast_seen.Schreib
session_expired(session, now).Lehne in
current_sessioneine abgelaufene Session ab und entferne sie, und bewegelast_seenbei einer aktiven.Nenne in Zeile F3 von
SECURITY.mddeinen Test und setz den Status aufclosed 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
Schreib in
tests/test_attacks.pytest_attack_idle_session_is_refused(). Verwendeclock = {"now": 1000}undstate["clock"] = helpers.fixed_clock(clock). Melde dich mitsteps.login_cookie(state, "ana", "correct horse")an. FragGET /memit demund sichere zu, dass der Benutzer anaist. Addierereading_server.IDLE_LIMIT + 1zuclock["now"], frag noch einmal und sichere zu, dass der BenutzerNoneist. Führpython -m pytest -qaus: Dieser Test muss zuerst fehlschlagen.Lies in
start_sessionnow = state["clock"]()und speichere es sowohl untercreatedals auch unterlast_seen.Schreib
session_expired(session, now):True, wennnow - session["last_seen"]größer alsIDLE_LIMITist odernow - session["created"]größer alsABSOLUTE_LIMIT.In
current_session, nachdem die Session gefunden ist: Lies die Zeit ausstate["clock"](); wenn die Session abgelaufen ist, entferne sie ausstate["sessions"]und gibNonezurück; andernfalls setz ihrlast_seenauf die aktuelle Zeit und gib sie zurück.Nenne in Zeile F3 von
SECURITY.mddeinen Test und setz den Status aufclosed 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.