Security & remediation
Independently audited across 11 security categories — 0 critical, 0 high. 8 of 12 verified findings fully resolved and 4 more partially mitigated; 33 areas examined and cleared.
Posture
What we checked & cleared
33 areas examined and found secure. 6 reported issues were rejected as false positives after deep-dive verification.
Findings — before & after
A shared friend-invite link could be claimed more than once
A friend-invite link's open slot had no single-use guard, so whoever opened the link first could take the slot, and it could even reopen if the match's server state was evicted — and the inviter could not tell who had joined.
A single-use flag now marks the slot claimed on first use and rejects later attempts; it is persisted in the room snapshot, so it survives server-state eviction and the slot never reopens. packages/partykit-server/src/match/match-room.ts:221-234
Anonymous account creation had no effective rate limit in production
The auth framework's rate limiting was effectively off in production because it defaulted to an environment flag that was never set, allowing unbounded anonymous account creation and database-quota / billing abuse.
An explicit rate-limit block is now enabled in code (5 requests per 60s, with the correct client-IP header for the Workers runtime), independent of any environment flag. apps/web/src/lib/server/auth.ts:103-113
Game-identity tokens could be issued for any display name without signing in
A token-issuing endpoint handed out player tokens for any chosen display name with no sign-in or identity check, allowing nick spoofing and leaderboard / queue noise.
The old anonymous endpoint now returns 410 Gone and is replaced by a web endpoint that requires an authenticated session, deriving identity and display name from the server-side account instead of client input. apps/web/src/routes/api/partykit-token/+server.ts:12-17
The hidden item's other attribute values were still sent to the client
When a round was sent to clients, only the challenged attribute of the hidden item was stripped — its other numeric attributes were still leaked, contradicting the documented promise and giving a small cheating aid.
The public round now strips all of the hidden item's attribute values, keeping only name and image, and a regression test asserts the values object is empty. packages/game-engine/src/public-round.ts:43-48
A match token was checked only at connect, not re-checked on later messages
The spec promised the match token would be re-checked on every message, but it was only verified once when the socket opened; afterwards the connection's side binding was trusted for the socket's lifetime, so an expired token could keep acting in a stalled match.
The server now re-checks the token's expiry on every privileged in-match message and drops a connection whose token has expired, matching the documented promise; a regression test covers it. packages/partykit-server/src/match/match-room.ts:130-133
The realtime service accepted requests from any website origin
The realtime endpoints answered every request with a wildcard cross-origin header, so any website's JavaScript could call them from a visitor's browser and read the response — letting a malicious page burn a visitor's rate-limit budget and drive lobby noise.
A strict Origin allowlist (the production domains plus configured origins) now replaces the wildcard, and unknown origins are blocked; a regression test asserts an unknown origin is denied. packages/partykit-server/src/util/cors.ts
Both sides of a 1v1 match could be driven by one player
One client could re-authenticate with a second identity on the same connection and bind itself to both sides of a 1v1 match, then play against itself to fabricate the result.
Authentication now rejects a second side on a connection that is already bound to one (with a conn_taken error), so a single connection can never hold both sides; a regression test covers it. packages/partykit-server/src/match/match-room.ts:222-228
A simultaneous-disconnect edge case could mis-assign a match result
The match room kept only a single shared forfeit timer, so if both players dropped within the 30-second grace window the second disconnect cancelled the first player's forfeit — letting a player who abandoned a losing match still come away with the win.
Each side now has its own forfeit timer plus a finished-match guard, so a second disconnect can no longer cancel the first player's forfeit or flip the winner; a regression test covers it. packages/partykit-server/src/match/match-room.ts:80,423-451
Browser protection headers are now sent on pages
Mitigated — protective headers and a content-security-policy ship on responses; a static-asset header file and a regression test are still tracked.
Weak or placeholder auth secrets are now rejected at startup
Mitigated — a minimum-length and leaked-placeholder check now fails startup on a weak key; a dedicated regression test is still tracked.
Outdated third-party libraries and no automated update process
Mitigated — the production-reachable libraries were pinned to safe versions; an automated CI dependency-audit gate is still tracked.
Limited visibility into suspicious activity
Mitigated — the key anti-cheat and authentication deny-points now emit log lines; structured / SIEM-ready logging is still tracked.
Severity & categories
How this was done
Found and fixed with an automated security pipeline
Recon mapped the attack surface, then category auditors swept auth, injection, crypto, concurrency, config, logging and dependencies in parallel. A deep-dive verifier re-checked every candidate and rejected 6 false positives, and a test-quality pass graded coverage. Each fix shown here was then verified directly in the current code — never taken on trust.