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:
jozamudi committed 2026-09-16 02:41:25 +00:00
1 parent 3e5c7062ab
commit 882263d276
2 files changed
+27 -7

No files matched your search

+21 -3
View File
@@ -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