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:
+9
-1
@@ -115,7 +115,11 @@ def check_password(conn, candidate: str) -> bool:
|
||||
|
||||
def set_password(conn, new_password: str):
|
||||
"""Store a new password and invalidate every outstanding session."""
|
||||
db.set_setting(conn, PASSWORD_KEY, hash_password(new_password))
|
||||
# Hash before taking the lock: scrypt is deliberately slow, and holding
|
||||
# SQLite's write lock across it would serialise unrelated writes.
|
||||
hashed = hash_password(new_password)
|
||||
db.begin_immediate(conn)
|
||||
db.set_setting(conn, PASSWORD_KEY, hashed)
|
||||
bump_epoch(conn)
|
||||
|
||||
|
||||
@@ -141,6 +145,10 @@ def current_epoch(conn) -> int:
|
||||
|
||||
|
||||
def bump_epoch(conn) -> int:
|
||||
# Read-modify-write, so it needs the write lock across both halves. Without
|
||||
# it, concurrent "sign out other devices" calls read the same epoch and
|
||||
# overwrite each other, and sessions that should have been cut off survive.
|
||||
db.begin_immediate(conn)
|
||||
epoch = current_epoch(conn) + 1
|
||||
db.set_setting(conn, EPOCH_KEY, epoch)
|
||||
return epoch
|
||||
|
||||
Reference in New Issue
Block a user