Wie wat mag, en hoe vaak · oefening
Antwoord 429 en weeg de blokkade af
Je hebt een teller die niemand iets vraagt. Kijk wat dat voor de login betekent. Voer in de
python pl_sec.py guessing
pl_sec: guessing, your server on 127.0.0.1 (a port the system chose)
--- six wrong passwords for ana ---
wrong password 1 POST /login -> 401
wrong password 2 POST /login -> 401
wrong password 3 POST /login -> 401
wrong password 4 POST /login -> 401
wrong password 5 POST /login -> 401
wrong password 6 POST /login -> 401
--- then her right password ---
ana's own password POST /login -> 200 {"user": "ana"}
(the clock moves 300 seconds)
ana's own password POST /login -> 200 {"user": "ana"}
--- result ---
6 wrong passwords, 0 refused before a password was checked
serve() returned after 8 connections; the server is closed
pl_sec: every thread has stopped
Zes gokken, zes wachtwoordcontroles, en niets zou zesduizend hebben tegengehouden. Dit is bevinding F9.
Schrijf eerst de aanval
De test stuurt reading_server.USER_LIMIT keer een verkeerd wachtwoord voor ana en controleert elke keer 401. Die asserts zijn de "retry-after" in de headers van het antwoord staat.
Lees de limiet uit de reading_server.USER_LIMIT, en schrijf het getal niet uit. Voer python -m pytest -q uit: 1 failed, 18 passed, met assert 401 == 429.
Vraag het voordat je controleert
login_wait(state, credentials) draait vóór login_user. Hij geeft 0 terug, of het aantal seconden dat de client moet wachten. Als hij wachten zegt, antwoordt de logintak 429 en kijkt hij helemaal niet naar het wachtwoord, zelfs niet als het wachtwoord klopt. Een limiet die de juiste gok doorlaat, is geen limiet op raden: de rader zou gewoon leren dat de gok juist was.
Het betekent ook dat een geweigerde poging je server geen scrypt-aanroep kost.
Waarom vraagt de server het de begrenzer voordat hij het wachtwoord controleert?
Hetzelfde voor een naam die niet bestaat
De sleutel is "user:" plus de naam die gestuurd werd, of die naam nu een gebruiker is of niet. Als alleen echte namen geteld werden, zouden alleen echte namen ooit 429 krijgen. De limiet zou een vreemde dan vertellen welke namen bestaan, en dat probeerde hoofdstuk 3 juist te verbergen.
Een verzoek zonder inloggegevens test geen wachtwoord. Het wordt niet geteld, en login_wait geeft er 0 voor terug.
Waarom pogingen tellen voor een naam die geen gebruiker is?
Een tweede, veel hogere limiet
Eén wachtwoord dat tegen 300 verschillende namen geprobeerd wordt, laat een teller per naam nooit afgaan. Daarom telt een tweede sleutel, "all", elke poging die de limiet per naam doorliet, tegen GLOBAL_LIMIT.
De volgorde doet ertoe. Vraag eerst de eigen limiet van de naam, en de globale limiet alleen als de eerste de poging toestond. Dan wordt één client die op één naam blijft hameren tegengehouden door de limiet van die naam, en gebruikt hij niet het deel van iedereen op.
Als de globale limiet weigert, is de poging al onder de naam vastgelegd. Neem die terug met de meegeleverde limits.forget_last(state["attempts"], key). Een geweigerde poging wordt nooit vastgelegd, onder geen van beide sleutels. Zonder die regel zou iemand die namen rondsproeit de tabel met namen vullen, en de eigen vijf pogingen van elke echte gebruiker opmaken terwijl de globale limiet vol is.
Het prefix user: houdt de twee soorten sleutels uit elkaar. Zonder dat prefix zou een gebruiker die all heet, een teller met iedereen delen.
Het antwoord
De meegeleverde too_many(wait) bouwt de weigering: status 429, een retry-after gezet op de wachttijd. De regel aan het eind van route voegt no-store toe, zoals bij elk antwoord van /login.
Verwijder na een juist wachtwoord de sleutel van die gebruiker met state["attempts"].pop("user:" + user, None). Niets anders zet een telling terug. Een mislukte poging mag haar nooit terugzetten.
Wat een limiet kost
Deze verdediging heeft een prijs, en je moet die kunnen benoemen.
Een limiet per naam laat iedereen ana buitensluiten. Die stuurt verkeerde wachtwoorden onder haar naam, en deze server kan die afzender op geen enkele manier van ana onderscheiden.
De globale limiet begrenst hoe snel iemand over alle namen heen kan raden. Eén client kan hem bereiken: 300 pogingen in 300 seconden is één verzoek per seconde. Zodra hij vol is, is elke gebruiker een venster lang buitengesloten. Een laag getal maakt dat goedkoop. Een hoger getal verhoogt alleen de kosten voor die client en neemt ze nooit weg.
Wat die client van iedereen anders zou onderscheiden, is iets wat deze verzoek-
En de limiet houdt een geduldige rader niet tegen. Vijf gokken per vijf minuten is 1440 per dag voor één naam. De limiet maakt van snel raden langzaam raden, en de volgende les maakt het zichtbaar.
Wat kan een vreemde ana met de limiet per naam aandoen, zonder haar wachtwoord te kennen?
GLOBAL_LIMIT is 300 pogingen in 300 seconden. Wat kan één client daarmee?
Je opdracht
Schrijf
test_attack_guessing_is_limited()en zie hem falen.Schrijf
login_wait(state, credentials): geen inloggegevens is 0; vraag anderslimits.allow_attemptvoor de sleutel van de gebruiker, daarna voor"all", en neem de eerste poging terug als het tweede antwoord weigert.Roep in de logintak
login_waitaan vóórlogin_user, antwoordtoo_many(wait)als de wachttijd boven 0 is, en verwijder de sleutel van de gebruiker na een juist wachtwoord.Vul rij F9 van
SECURITY.md, noem je test, en zet de status opclosed in 6.4.
Voer python -m pytest -q uit: 19 passed. Voer daarna python pl_sec.py guessing opnieuw uit. De eerste vijf verkeerde wachtwoorden krijgen nog 401. De zesde regel luidt nu:
wrong password 6 POST /login -> 429 retry-after: 300
Het juiste wachtwoord van ana krijgt ook 429, tot de klok 300 seconden verder is, en de run eindigt met the limit answered 429 after 5 attempts; Retry-After: 300.
Je server weigert nu op vier manieren: 401, 403, 404 en 429. Hij schrijft er niets van op.
Opdracht
Zet de begrenzer vóór de login.
Schrijf in
tests/test_attacks.pytest_attack_guessing_is_limited(). Stuur metstate = steps.keyed_state()reading_server.USER_LIMITkeerPOST /loginvoor ana met een verkeerd wachtwoord ({"Authorization": helpers.basic("ana", "a guess")}als headers), en controleer elke keer status 401. Stuur het nog één keer en controleer status 429 en dat"retry-after"in de headers van het antwoord staat. Voerpython -m pytest -quit: deze test moet eerst falen.Schrijf in
reading_server.pylogin_wait(state, credentials). Zonder inloggegevens geef je 0 terug. Leesnowuitstate["clock"](). De sleutel van de gebruiker is"user:" + credentials["user"]. Vraaglimits.allow_attempt(state["attempts"], key, now, limit=USER_LIMIT, window=WINDOW). Als die 0 teruggeeft, vraag dan opnieuw met de sleutel"all"enlimit=GLOBAL_LIMIT; als dit tweede antwoord boven 0 is, neem de eerste poging terug metlimits.forget_last(state["attempts"], key). Geef de wachttijd terug.In de logintak van
route: haal de inloggegevens op, roeplogin_waitaan, en antwoordtoo_many(wait)als de wachttijd boven 0 is, zonderlogin_useraan te roepen;too_manywordt meegeleverd. Verwijder na een juist wachtwoord de sleutel van die gebruiker metstate["attempts"].pop("user:" + user, None).Vul rij F9 van
SECURITY.md, noem je test in de cel Test, en zet de status opclosed in 6.4.
Voer python -m pytest -q uit tot het eindigt met 19 passed, en klik daarna op Check my work. Die zet USER_LIMIT, GLOBAL_LIMIT en WINDOW op eigen, kleinere getallen en telt antwoorden onder een klok die hij bestuurt: verkeerde wachtwoorden tot aan de limiet en één meer, het juiste wachtwoord tijdens de wachttijd en na het venster, een naam die niet bestaat naast een die wel bestaat, de telling na een geslaagde login, verzoeken zonder inloggegevens, één naam die vaak geprobeerd wordt, één wachtwoord dat onder veel namen geprobeerd wordt, en een gebruiker die all heet. Hij telt scrypt-aanroepen om te zien dat een geweigerde poging geen wachtwoord controleert; hij meet geen tijd. Hij voert je test vier keer uit, tegen servers die hij op de plaats van reading_server.handle en reading_server.sample_state zet, elk met een eigen reading_server.USER_LIMIT: een correcte, waar je test moet slagen; een die een login nooit begrenst, een waarvan de 429 geen Retry-After heeft, en een die elk verzoek weigert, waar hij moet falen. Hij leest rij F9 van SECURITY.md. Niets uit je