Sessies die standhouden · oefening
Sessies die eindigen
Open SECURITY.md en lees rij F3, die je in hoofdstuk 1 schreef: een Max-Age=3600, en dat vraagt een client alleen om zijn kopie na een uur te vergeten. Een kopie van de id in een bestand vergeet niets, en state["sessions"] houdt de vermelding zo lang als de server draait. Deze les geeft een sessie een einde waarover de server beslist.
Twee limieten, voor twee aanvallers
Een sessie kan op twee manieren misbruikt worden, en elk vraagt een eigen limiet.
Een inactiviteitslimiet (idle limit) beëindigt een sessie die niemand gebruikt. Die dekt een laptop die open in een bibliotheek blijft staan en een cookie die op een gedeelde computer is achtergebleven: na 30 minuten zonder verzoek is de id niets meer waard.
Een absolute limiet beëindigt een sessie die iemand blijft gebruiken. Een dief die een id kopieerde, kan om de paar minuten een verzoek sturen, zodat de sessie nooit inactief is. Alleen haar leeftijd kan haar beëindigen: 8 uur na de login is de sessie voorbij, hoe druk ze ook was.
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
De eerste regel laat een sessie zien met een gat van 31 minuten: het verzoek na het gat wordt geweigerd. De tweede laat een sessie zien zonder enig gat: het eerste verzoek na de grens van 8 uur wordt geweigerd. De twee getallen staan in reading_server.py als IDLE_LIMIT = 1800 en ABSOLUTE_LIMIT = 28800, in seconden. Het is de keuze van deze dienst, geen standaard. Een dienst die meer te beschermen heeft, kiest kortere limieten.
Een dief kopieert een sessie-id en stuurt er elke tien minuten één verzoek mee. Welke limiet beëindigt die sessie?
Tijd is een argument
Beide limieten vergelijken een opgeslagen tijd met de tijd van nu. De server haalt de tijd uit state["clock"], een sample_state() zet die op time.time, de echte klok. Je code roept state["clock"]() aan en roept nooit zelf time.time() aan. Dat is de geïnjecteerde tijd uit Python in Practice: omdat de klok via state binnenkomt, kan een test bepalen hoe laat het is.
helpers.fixed_clock(holder) geeft een klokfunctie terug die de tijd uit holder["now"] leest. De test bewaart de
clock = {"now": 1000}
state["clock"] = helpers.fixed_clock(clock)
Na clock["now"] += 1801 zijn er voor de server dertig minuten en één seconde verstreken. Er wacht niets.
Schrijf eerst de aanval
De aanvaller doet hier niets slims. Hij gebruikt een id lang nadat de eigenaar ermee gestopt is. De tweede stap in tests/steps.py houdt de test kort: steps.login_cookie(state, user, password) logt in via handle() en geeft het cookiepaar terug, of None als de login geweigerd werd.
De test zet de vaste klok, logt in als ana, en vraagt GET /me met de cookie. Hij controleert dat de gebruiker ana is, en dat is de reading_server.IDLE_LIMIT + 1 op bij clock["now"], vraagt opnieuw, en controleert dat de gebruiker None is.
Voer python -m pytest -q uit. De run eindigt met 1 failed, 9 passed, en de falende regel is weer AssertionError: assert 'ana' is None: een half uur later antwoordt de sessie nog steeds.
Eén pure vraag
Een sessie heeft twee tijdstempels nodig om voor zichzelf te antwoorden. start_session leest één keer now = state["clock"]() en slaat die op onder zowel created als last_seen.
session_expired(session, now) beantwoordt dan één vraag met twee aftrekkingen en twee vergelijkingen. now - session["last_seen"] is hoe lang de sessie inactief is geweest, en now - session["created"] is hoe oud ze is. De functie geeft True terug als het eerste meer is dan IDLE_LIMIT of het tweede meer dan ABSOLUTE_LIMIT. Ze leest geen klok en verandert niets, dus je kunt haar met willekeurige getallen uitproberen.
Wat drukt dit af?
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)
Weigeren, verwijderen en één veld bijwerken
In current_session(request, state) treden de limieten in werking, omdat /me, /prefs en session_user er allemaal doorheen gaan. Geen enkele aanroeper kan de limieten overslaan. Nadat hij de sessie gevonden heeft, leest hij de tijd en vraagt hij het aan session_expired:
Een verlopen sessie wordt uit
state["sessions"]verwijderd, en de functie geeftNoneterug, precies alsof de cookie niets aanduidde. Verwijderen doet ertoe. Een sessie die geweigerd en bewaard wordt, staat nog in elke kopie van de state, en één veranderde regel zou haar terug kunnen brengen.Een levende sessie krijgt
last_seen = now, en de functie geeft haar terug.
created verschuift nooit. Als een verzoek die
De cookie zegt Max-Age=3600. Een aanvaller bewaarde de id in een bestand. Wat houdt de id twee uur later, als niemand de sessie gebruikt heeft, tegen?
Er is nog één gebeurtenis die sessies zou moeten beëindigen: als een wachtwoord verandert, zou elke sessie van die gebruiker moeten eindigen. Deze dienst heeft geen route die een wachtwoord verandert, dus hier valt niets te bouwen.
Je opdracht
Schrijf in
tests/test_attacks.pytest_attack_idle_session_is_refused()en zie hem falen.Sla in
start_sessionde tijd op ondercreatedenlast_seen.Schrijf
session_expired(session, now).Weiger en verwijder in
current_sessioneen verlopen sessie, en werklast_seenbij in een levende.Noem in rij F3 van
SECURITY.mdje test en zet de status opclosed in 4.3.
Voer python -m pytest -q opnieuw uit. Het eindigt met 10 passed: de inactiviteitstest, de fixatietest uit les 2, en de acht uit eerdere hoofdstukken.
Klik daarna op Check my work. Sessies eindigen nu op de server. De cookie die de id draagt, zegt nog steeds alleen HttpOnly, Max-Age en Path, en de volgende les geeft hem de rest van zijn instructies.
Opdracht
Schrijf de aanval met een inactieve
Schrijf in
tests/test_attacks.pytest_attack_idle_session_is_refused(). Gebruikclock = {"now": 1000}enstate["clock"] = helpers.fixed_clock(clock). Log in metsteps.login_cookie(state, "ana", "correct horse"). VraagGET /memet deen controleer dat de gebruiker anais. Telreading_server.IDLE_LIMIT + 1op bijclock["now"], vraag opnieuw, en controleer dat de gebruikerNoneis. Voerpython -m pytest -quit: deze test moet eerst falen.Lees in
start_sessionnow = state["clock"]()en sla die op onder zowelcreatedalslast_seen.Schrijf
session_expired(session, now):Truealsnow - session["last_seen"]meer is danIDLE_LIMIT, ofnow - session["created"]meer is danABSOLUTE_LIMIT.In
current_session, nadat de sessie gevonden is: lees de tijd uitstate["clock"](); als de sessie verlopen is, verwijder haar uitstate["sessions"]en geefNoneterug; zet anders haarlast_seenop nu en geef haar terug.Noem in rij F3 van
SECURITY.mdje test en zet de status opclosed in 4.3.
Voer python -m pytest -q uit in de 10 passed.
Klik daarna op Check my work. Die roept session_expired aan met eigen sessies, één seconde binnen en één seconde buiten elke limiet, dus > en >= worden allebei goedgekeurd. Onder een klok die hij verzet, ver van de echte tijd, roept hij start_session aan en leest hij de twee tijdstempels, en daarna vraagt hij GET /me via handle(): 100 seconden later moet de sessie antwoorden en mag alleen last_seen verschoven zijn; voorbij IDLE_LIMIT moet ze null antwoorden en uit state["sessions"] verdwenen zijn; en een sessie die telkens vlak voor elke inactiviteitslimiet opnieuw gevraagd wordt, moet eindigen zodra ABSOLUTE_LIMIT verstreken is. Hij voert je test drie keer uit, elke keer tegen een server die hij op de plaats van reading_server.handle en reading_server.sample_state zet: een die inactieve sessies beëindigt, waar je test moet slagen; een die dat nooit doet, waar hij moet falen; en een die elke login weigert, waar hij moet falen. Als laatste leest hij rij F3 van SECURITY.md op de naam van je test en een status die begint met closed. Niets uit je terminal wordt beoordeeld.