Throttle the change-password route and make epoch bumps atomic
Two session-management bugs from the audit: - /api/password recorded failed attempts against the login budget but never checked it, so a borrowed session could guess the current password without limit while still locking the owner out of /api/login. Verified: thirteen consecutive wrong guesses all returned 403 and none returned 429. It now spends from the same budget it was topping up. - bump_epoch read the epoch and wrote it back without the write lock, so concurrent revocations lost increments and sessions that should have been cut off survived. Verified: twenty concurrent bumps advanced the counter from 2 to 6, and twenty concurrent "sign out other devices" calls left four sessions authenticated. It takes BEGIN IMMEDIATE now; set_password hashes before locking, so scrypt doesn't serialise unrelated writes. Removing either fix makes its test fail with exactly that symptom. README corrections: deployment is rsync, not git pull — gitea on .8 cannot serve a clone to .8 itself, which the deploy section now documents — and the concurrency check count was understated. Checks go from 182 to 197. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
@@ -367,6 +367,16 @@ async def change_password(
|
||||
if not auth.auth_enabled():
|
||||
raise HTTPException(400, "Authentication is disabled, so there is no password to change")
|
||||
|
||||
# This route verifies the same credential the login form does, so it has to
|
||||
# honour the same budget. It was recording failures without checking them,
|
||||
# which meant a borrowed session could guess the password without limit —
|
||||
# while still locking the owner out of /api/login.
|
||||
retry_after = auth.login_retry_after()
|
||||
if retry_after:
|
||||
raise HTTPException(
|
||||
429, "Too many failed attempts", headers={"Retry-After": str(retry_after)}
|
||||
)
|
||||
|
||||
if not auth.check_password(conn, body.current_password):
|
||||
auth.record_failure()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
Reference in New Issue
Block a user