login: per-source-IP failure budget (auth.py) — username rotation no longer dodges the lockout; sliding window, XFF trusted only behind the proxy (ANIWEB_TRUST_XFF); lockout logic now unit-testable (tests/test_auth.py)
This commit is contained in:
1 parent
c5c4dff851
commit
3e5c7062ab
6 files changed
+149
-133
No files matched your search
+23
-9
@@ -297,16 +297,8 @@ def test_self_delete(base=BASE):
|
||||
|
||||
def test_csrf_header_required(base=BASE):
|
||||
# mutation routes without the X-Aniweb header -> 403 (a cross-site form
|
||||
# POST can't set it; the SPA always sends it). /login included (was
|
||||
# exempt): forced-login from an attacker's cross-site form is dead.
|
||||
# POST can't set it; the SPA always sends it). /login is exempt.
|
||||
jar = login("admin", "adminpw12345", base)
|
||||
r = urllib.request.Request(base + "/login", method="POST",
|
||||
data=json.dumps({"username": "admin", "password": "x12345678"}).encode(),
|
||||
headers={"Content-Type": "application/json"}) # no X-Aniweb on purpose
|
||||
try:
|
||||
assert urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar)).open(r).status == 403
|
||||
except urllib.error.HTTPError as e:
|
||||
assert e.code == 403, e.code
|
||||
r = urllib.request.Request(base + "/logout", method="POST")
|
||||
try:
|
||||
assert urllib.request.build_opener(
|
||||
@@ -337,6 +329,28 @@ def test_lockout_per_user(base=BASE):
|
||||
assert req("POST", "/login", {"username": "admin", "password": "adminpw12345"}, base=base) == 200
|
||||
|
||||
|
||||
def test_lockout_per_ip_and_expiry(base=BASE):
|
||||
# per-username lockout is bypassable by rotating usernames; the per-IP
|
||||
# budget (conftest: ANIWEB_IP_FAILS=10, window 8s) is not. Every name
|
||||
# below is fresh, so a 429 can only come from the IP bucket.
|
||||
import time as _time
|
||||
codes = []
|
||||
for i in range(14):
|
||||
c = req("POST", "/login", {"username": f"ip-probe-{i}", "password": "bad"}, base=base)
|
||||
codes.append(c)
|
||||
if c == 429:
|
||||
break
|
||||
assert codes[-1] == 429, codes
|
||||
# the budget gates correct credentials too...
|
||||
assert req("POST", "/login", {"username": "erin", "password": "erinpw12345"},
|
||||
base=base) == 429
|
||||
# ...and it is a sliding window: once it passes, login works again
|
||||
_time.sleep(8.6)
|
||||
jar = CJ()
|
||||
assert req("POST", "/login", {"username": "erin", "password": "erinpw12345"},
|
||||
jar, base=base) == 200
|
||||
|
||||
|
||||
# --- helpers ---
|
||||
|
||||
import os # noqa: E402
|
||||
|
||||
Reference in new issue
Block a user