Harden seat transfer phrases (security review finding)

Three words from 32 was ~15 bits — enumerable within a code's
lifetime against an unthrottled endpoint. Now: 64-word list, 4-word
phrases (~24 bits / 16.7M combinations), a per-connection cap of 5
failed claims, and a global backstop that voids all pending codes and
cools the endpoint after 30 failures in 10 minutes. Codes stay
speakable; a brute force now gets ~35 guesses against 16.7M before
the door closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 00:00:13 -04:00
co-authored by Claude Fable 5
parent 2184721ab4
commit ce8b6a315b
2 changed files with 36 additions and 5 deletions
+10 -2
View File
@@ -36,6 +36,7 @@ interface Session {
socket: WebSocket;
playerId: PlayerId | null;
roomId: string | null;
claimFails: number;
}
const sessions = new Set<Session>();
@@ -70,7 +71,7 @@ function broadcastRoomState(room: Room): void {
}
wss.on("connection", (socket) => {
const session: Session = { socket, playerId: null, roomId: null };
const session: Session = { socket, playerId: null, roomId: null, claimFails: 0 };
sessions.add(session);
send(socket, { type: "welcome", game: "wizwar" });
@@ -142,8 +143,15 @@ wss.on("connection", (socket) => {
break;
}
case "claimTransfer": {
if (session.claimFails >= 5) {
return send(socket, { type: "error", message: "too many attempts on this connection — reconnect and mint a fresh phrase" });
}
const result = claimTransferCode(String(msg.code ?? ""));
if ("error" in result) return send(socket, { type: "error", message: result.error });
if ("error" in result) {
session.claimFails++;
return send(socket, { type: "error", message: result.error });
}
session.claimFails = 0;
send(socket, { type: "transferClaimed", seat: result });
break;
}