Extend LoginAttemptOk throttle to 4 sibling auth handlers#268
Conversation
PR #267 added a SHA-256 dummy-hash to the no-account paths of P_StartGame, P_FetchCharacter, P_CreateCharacter, P_DeleteCharacter (plus P_VerifyAccount, P_ChangePassword) to close the no-account-vs-wrong-password timing oracle. The fix worked but inverted the cost profile of the four sibling handlers: each malformed-username packet now costs a full SHA-256, and four of the six handlers didn't wear the LoginAttemptOk throttle that P_VerifyAccount uses. An attacker could pump SHA-256 cost at line-rate by sending bogus P_StartGame / P_FetchCharacter / P_CreateCharacter / P_DeleteCharacter packets, dodging the per-FromID throttle. Reviewer-flagged in iteration #8 (PR #267) as deferred follow-up. This PR extends the same gate that protects P_VerifyAccount to all four sibling handlers: - LoginAttemptOk(M\FromID) pre-check at the top of each Case body. If throttled, emit the canonical "N" failure and skip the rest. - LoginAttemptRecord(M\FromID, False) on the wrong-password Else branch inside the For loop. - LoginAttemptRecord(M\FromID, False) on the no-account Exists=False branch (alongside the existing #267 dummy-hash call). - LoginAttemptRecord(M\FromID, True) on the success exit (the one spot where the password actually verified and the handler reached its happy-path RCE_Send). The shared per-FromID counter means an attacker can't dodge the throttle by rotating across handler types -- 5 auth-flavored Cases now contribute to the same window. Post-auth failure paths (bad character index, name validation failure, no free slots, bad ActorID) are deliberately not recorded: they require valid credentials to reach and a legitimate user could trigger them via input typos. P_ChangePassword is NOT covered in this PR. Its session-gating (RequesterOwnsAccountSession) bounds the actual destructive operation, but its no-account dummy-hash call (added in #267) IS still unbounded. Worth a follow-up iteration -- the one-line throttle gate pattern from this PR is the template. Verified: full compile.bat clean across Server + Client + GUE + Project Manager + 7 Tools targets (exit 0, unfiltered output). test.bat green 19/19 (no new tests required -- the behaviour-observable contract is "throttled clients get a canonical failure code", which matches the unthrottled failure code and is already covered by the existing AccountEnumerationTest / ChangePasswordEnumerationTest negative-space sweeps for "N" never emitted"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82410986c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| LoginAttemptRecord(M\FromID, False) | ||
| RCE_Send(Host, M\FromID, P_StartGame, "N", True) |
There was a problem hiding this comment.
Do not count post-auth slot errors as failed logins
This branch records a failed login attempt even though password verification has already succeeded and the failure is only an invalid/empty character slot. Because LoginAttemptRecord feeds the shared LoginAttemptOk gate, a legitimate user who clicks a bad slot a few times can be throttled out of unrelated auth handlers for 60s despite correct credentials; before this commit this path only returned "N" without consuming the auth-failure budget.
Useful? React with 👍 / 👎.
| LoginAttemptRecord(M\FromID, False) | ||
| RCE_Send(Host, M\FromID, P_FetchCharacter, "N", True) |
There was a problem hiding this comment.
Avoid throttling on authenticated fetch-slot validation failures
Here the code records a failed login for a post-auth validation error (P_FetchCharacter with an invalid character index). Since the password check already passed, counting this as an auth failure can incorrectly trip LoginAttemptOk and temporarily block valid users from login-related flows after repeated UI/client mistakes; this behavior is new in this commit and was not previously counted.
Useful? React with 👍 / 👎.
Summary (non-technical)
The recent timing-oracle fix (#267) added a SHA-256 cost to four character-management handlers so attackers couldn't distinguish "unknown account" from "wrong password" by response time. But those four handlers don't share the per-connection rate-limit that the main login does — so an attacker could pump SHA-256 work at line-rate by spamming bogus character-fetch / create / delete / start-game packets. This PR plugs that gap by extending the same throttle to all four siblings.
Technical summary
PR #267 added
VerifyPassword("", IncomingPwd)to the no-account paths ofP_StartGame,P_FetchCharacter,P_CreateCharacter,P_DeleteCharacter(plusP_VerifyAccount,P_ChangePassword) so the timing matched the wrong-password path. That closed the timing oracle but inverted the cost profile — each malformed packet now pays a SHA-256 round, and the four sibling handlers didn't wear theLoginAttemptOkthrottle that gatesP_VerifyAccount. Reviewer-flagged in iteration #8 as deferred follow-up.This PR mirrors the
P_VerifyAccountgating pattern across the four sibling handlers:CasebodyIf Not LoginAttemptOk(M\FromID) Then RCE_Send "N" Else <body> EndIfLoginAttemptRecord(M\FromID, False)beforeRCE_Send "N"LoginAttemptRecord(M\FromID, False)alongside the existing #267 dummy-hashLoginAttemptRecord(M\FromID, True)before the successRCE_SendThe shared per-
FromIDcounter means an attacker can't dodge the throttle by rotating across handler types — all 5 auth-flavored Cases (P_VerifyAccount+ the 4 siblings) now contribute to the same window.Post-auth failure paths deliberately NOT recorded: bad character index, name validation failure, no free slots, bad ActorID, character-area-not-found. These require valid credentials to reach and a legitimate user could trigger them via input typos — penalising them would block legitimate retries without security benefit.
Acceptance criteria
LoginAttemptOkpre-check at the top of its Case bodyLoginAttemptRecord(M\FromID, False)on the wrong-password and no-account pathsLoginAttemptRecord(M\FromID, True)on the success exitcompile.batclean across Server + Client + GUE + Project Manager + 7 Tools (exit 0, unfiltered)test.bat19/19 greenTrade-offs / deferred
P_ChangePasswordnot covered. Its session-gating (RequesterOwnsAccountSession) bounds the actual destructive op, but its no-account dummy-hash call (added in Close VerifyPassword% timing oracle (constant-time compare + dummy hash on no-account path) #267) is still unbounded. The one-line throttle gate pattern from this PR is the template — separate iteration.LoginAttemptRecord(False)could be added per-site, but doing it preemptively trades legitimate-user friction for theoretical hardening.AccountEnumerationTestandChangePasswordEnumerationTest's negative-space sweeps. A network-level throttle integration test would need a packet-handler harness that doesn't exist yet; out of scope.P_VerifyAccountitself unchanged — it already had the gate from before Close VerifyPassword% timing oracle (constant-time compare + dummy hash on no-account path) #267.Risk
LoginAttemptMaxFailures/LoginAttemptWindowMsin AccountsServer.bb) is already tuned forP_VerifyAccount, which is the dominant legitimate-traffic auth source. A single client hits the four sibling handlers at most once per session (start game, fetch character, create character, delete character). Applying the same threshold cannot block legitimate clients beforeP_VerifyAccountwould.🤖 Generated with Claude Code