Sessies die standhouden · oefening
Een nieuwe id bij de login
Les 1 volgde de fixatieaanval op papier. Voer hem uit tegen je server:
python pl_sec.py fixation
Het hulpprogramma start je server op
--- the attacker gets a session without logging in ---
the attacker saves a preference POST /prefs -> 200
the attacker now knows this cookie: session=<22 characters>
--- the victim logs in ---
the victim's browser now holds the attacker's cookie (pl_sec put it there)
ana logs in, sending that cookie POST /login -> 200
the login reply set this cookie: session=<the same 22 characters>
--- who is who now ---
the attacker, with the cookie from before the login GET /me -> 200 {"user": "ana", "page_size": 5}
ana, with the cookie her login reply set GET /me -> 200 {"user": "ana", "page_size": 5}
--- result ---
the id the attacker knew now answers as: ana
Het hulpprogramma laat de kant van de server van de aanval zien. De stap waarin de id de browser van het slachtoffer bereikt, is één regel tekst, (pl_sec put it there), omdat niets hier een browser is.
Schrijf eerst de aanval
De aanval wordt een test voordat je iets repareert. Open tests/steps.py, dat nieuw is in dit hoofdstuk. Het bevat stappen die een bezoeker zet, één keer opgeschreven zodat je tests kort blijven. Deze les heeft er één van nodig: steps.cookie_pair(response) geeft de tekst name=value terug van de Cookie, of None als het antwoord geen cookie zet.
De test heeft vier stappen:
Bouw een state met
reading_server.sample_state().De aanvaller: stuur
POST /prefsmet de data{"page_size": 5}en bewaarsteps.cookie_pair(...)van het antwoord. Dat is de id die de aanvaller kent.Het slachtoffer: stuur
POST /loginvoor ana met haar wachtwoord en die cookie, en controleer dat de status 200 is. Dit is devooraf. Zonder die voorwaarde zou een server die elke login weigert de test doorstaan, en zou hij niets gesloten hebben. Weer de aanvaller: stuur
GET /memet dezelfde cookie en controleer dat de gebruikerNoneis.
Het loginverzoek draagt twee headers, de inloggegevens en de geplante cookie:
headers = {"Authorization": helpers.basic("ana", "correct horse"), "Cookie": planted}
Voer de tests uit:
python -m pytest -q
De run eindigt met 1 failed, 8 passed, en de falende regel luidt AssertionError: assert 'ana' is None. Die mislukking is bevinding F5.
Een nieuwe id, en bijna niets meegenomen
rotate_session(state, old_id, user) doet drie dingen. Hij verwijdert de vermelding die old_id aanduidt uit state["sessions"]. Hij start een nieuwe sessie voor user met start_session, die een nieuwe id maakt met secrets.token_urlsafe(16). En hij neemt één page_size=... mee te geven.
Verder kopieert hij niets. Wat een bezoekerssessie ook bevat, het is er vóór de login in gezet, door wie de id ook had, en dat kan de aanvaller zijn. Vandaag heeft een sessie twee velden, dus alles kopiëren zou geen kwaad doen. Latere hoofdstukken voegen velden toe. Een
Waarom kopieert rotate_session page_size en verder niets uit de oude sessie?
Drie aanroepers
De logintak roept rotate_session(state, session_id_from(request), user) aan, en session_id_from kan drie soorten waarden teruggeven:
None, voor een login zonder cookie.Een id die de server nooit heeft uitgegeven, omdat een client elke tekst als cookie kan sturen.
Een id die een levende sessie aanduidt, van een bezoeker of van een eerdere login.
Alle drie moeten op dezelfde manier eindigen: één nieuwe vermelding voor de gebruiker, en geen vermelding onder de oude id. state["sessions"].pop(old_id, None) geeft de verwijderde sessie terug, of None als geen sessie die id heeft, dus één aanroep dekt de laatste twee gevallen. Als er geen oude sessie was, krijgt de nieuwe DEFAULT_PAGE_SIZE.
Een cache mag geen Set-Cookie bewaren
Een header Set-Cookie houdt een cache niet tegen om een antwoord op te slaan. Een gedeelde cache die er een opsloeg, zou dezelfde sessie-id aan elke volgende bezoeker geven. De standaarden houden een cache die zich eraan houdt al weg van de login van deze dienst, die een POST met een header Authorization is. De regel hieronder is voor elk antwoord dat ze een cache wel zouden laten bewaren, en voor caches die zo zijn ingesteld dat ze meer bewaren dan zou moeten.
De regel: een antwoord dat een cookie zet, zegt Cache-Control: no-store, en dat doet ook elke sessieroute. Het eind van handle() heeft er een deel van, uit
if segments == ["login"] or segments == ["logout"] or segments == ["me"]:
response["headers"]["cache-control"] = "no-store"
/prefs ontbreekt. Die kwam mee met de bezoekerssessie, hij zet een cookie, en niemand heeft hem aan de regel toegevoegd. Voeg hem toe aan de no-store zet zodra "set-cookie" in response["headers"] staat. De tweede regel dekt een route die iemand volgend jaar toevoegt en vergeet op te nemen.
Een andere dienst start een bezoekerssessie bij GET /, met een header Set-Cookie en zonder Cache-Control. Een gedeelde cache ervoor slaat dat antwoord op en serveert het aan de volgende bezoeker. Wat krijgt die bezoeker?
Je opdracht
Schrijf in
tests/test_attacks.pytest_attack_fixed_session_is_not_promoted()en zie hem falen.Schrijf in
reading_server.pyrotate_session(state, old_id, user).Vervang in de logintak de vier meegeleverde regels door
session_id = rotate_session(state, session_id_from(request), user).Laat aan het eind van
handle()de regel voorno-storegelden voor/prefsen voor elk antwoord met een headerset-cookie.Vul rij F5 van
SECURITY.md: wat er blootgesteld is, de aanval, de oplossing, en de naam van je test.
Voer python -m pytest -q opnieuw uit. Het eindigt nu met 9 passed. Voer daarna python pl_sec.py fixation nog eens uit. Het login-antwoord zet een andere cookie dan die de aanvaller kende, de GET /me van de aanvaller antwoordt {"user": null, "page_size": 20}, die van ana antwoordt {"user": "ana", "page_size": 5}, en de resultaatregel luidt the id the attacker knew now answers as: nobody.
Een bevinding is gesloten als haar test slaagt, dus zet de status van F5 op closed in 4.2.
Klik daarna op Check my work. Een login is nu veilig op het moment dat hij gebeurt. Na dat moment leeft de sessie zo lang als de server draait, en dat verandert de volgende les.
Opdracht
Schrijf de fixatieaanval als test, en geef daarna elke login een nieuwe
Schrijf in
tests/test_attacks.pytest_attack_fixed_session_is_not_promoted(). Bouw een state metreading_server.sample_state(). StuurPOST /prefsmet de data{"page_size": 5}en bewaarsteps.cookie_pair(...)van het antwoord: dat is de id die de aanvaller kent. StuurPOST /loginvoor ana met haar wachtwoord en diein een header Cookie, en controleer status 200. StuurGET /memet dezelfde cookie en controleer dathelpers.json_of(...)["user"]Noneis. Voerpython -m pytest -quit: deze test moet falen.Schrijf in
reading_server.pyrotate_session(state, old_id, user). Verwijder de vermelding dieold_idaanduidt uitstate["sessions"]als die er is;old_idkanNonezijn of een id die de server nooit heeft uitgegeven. Start de nieuwe sessie metstart_session(state, user, page_size=...), met de paginagrootte van de oude sessie, ofDEFAULT_PAGE_SIZEals er geen oude sessie was. Geef de nieuwe id terug.Vervang in de logintak de vier meegeleverde regels door
session_id = rotate_session(state, session_id_from(request), user).Laat aan het eind van
handle()de regel voorno-storegelden voor/prefsen voor elk antwoord met een headerset-cookie.Vul rij F5 van
SECURITY.md, noem je test erin, en zet de status opclosed in 4.2zodra de test slaagt.
Voer python -m pytest -q uit in de 9 passed, en python pl_sec.py fixation om de aanval te zien mislukken.
Klik daarna op Check my work. Die roept rotate_session aan met een eigen state: een bezoekerssessie met een paginagrootte en één extra sleutel, geen oude sessie, en een id die de server nooit heeft uitgegeven. De oude vermelding moet weg zijn, de nieuwe sessie moet de gebruiker en de paginagrootte bevatten en verder niets uit de oude, en de nieuwe id moet uit secrets komen met 16 bytes of meer; een rotate_session die de id zelf maakt met secrets.token_hex(16) wordt geaccepteerd. Daarna roept hij handle() aan met gebruikers die hij je niet heeft laten zien: POST /prefs, een login die de cookie van de bezoeker stuurt, en GET /me met de oude en de nieuwe cookie, en hij leest Cache-Control bij POST /prefs en bij de login. 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 bij de login een nieuwe id uitgeeft, waar je test moet slagen; een die de id houdt, waar hij moet falen; en een die elke login weigert, waar hij moet falen. Als laatste leest hij rij F5 van SECURITY.md: de cellen Exposed, Attack en Fix hebben tekst nodig, de cel Test heeft de naam van je test nodig, en de status moet beginnen met closed. De formulering van de rij wordt niet beoordeeld. Niets uit je terminal wordt beoordeeld.