login POST now requires the X-Aniweb header: cross-site forced-login (attacker-form session swap) dies at the 403; login.html sends the header like the SPA
This commit is contained in:
1 parent
3e5c7062ab
commit
882263d276
2 files changed
+27
-7
No files matched your search
+21
-3
@@ -297,7 +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 is exempt.
|
||||
# POST can't set it; the SPA always sends it). /login is covered too now
|
||||
# (see test_login_requires_custom_header — forced-login was the hole).
|
||||
jar = login("admin", "adminpw12345", base)
|
||||
r = urllib.request.Request(base + "/logout", method="POST")
|
||||
try:
|
||||
@@ -320,9 +321,26 @@ def test_stream_token_host_allowlist(base=BASE):
|
||||
assert req("GET", f"/stream/{bad2}", jar=jar, base=base) == 400
|
||||
|
||||
|
||||
def test_login_requires_custom_header(base=BASE):
|
||||
# forced-login CSRF: /login was the one POST route exempt from the custom
|
||||
# header check, and login needs no cookie — so any cross-site <form>
|
||||
# POST could silently sign a visiting browser into the attacker's account.
|
||||
# /login now demands the header like every other mutation; a cross-site
|
||||
# form cannot set it, so the whole attack class dies at the 403.
|
||||
r = urllib.request.Request(base + "/login", method="POST",
|
||||
data=json.dumps({"username": "admin",
|
||||
"password": "adminpw12345"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
urllib.request.build_opener().open(r)
|
||||
raise AssertionError("POST /login without X-Aniweb must be 403")
|
||||
except urllib.error.HTTPError as e:
|
||||
assert e.code == 403, e.code
|
||||
|
||||
|
||||
def test_lockout_per_user(base=BASE):
|
||||
# a throwaway name: the lockout is per-username and in-memory, and bob is
|
||||
# also used by the browser tests (a locked-out bob breaks their logins)
|
||||
# a throwaway name: the buckets are in-memory and per-username, and bob
|
||||
# is also used by the browser tests (a locked-out bob breaks their logins)
|
||||
codes = [req("POST", "/login", {"username": "lockout-ghost", "password": "bad"}, base=base)
|
||||
for _ in range(6)]
|
||||
assert codes[:5] == [401] * 5 and codes[5] == 429, codes
|
||||
|
||||
Reference in new issue
Block a user