← Server architecture · all tasks

Server hardening batch — the 010 audit's security fixes

done priority 2 estimate M

server-architecture/014 · created 2026-08-13 · updated 2026-08-13

What

The 010 audit (../010-accounts-audit.md) — recommendation 1’s batch, split out from task 013 (which takes the client-side findings) by Kris’s call on 2026-08-13. All small, local, independently testable server fixes; none touches the grant architecture. Work in land-of-lor/game/director/ (plus one world_server.gd change for the takeover reason).

The batch (audit reference in parens)

  1. Revoke all sessions on password reset (H1) — call the existing, tested revokeAllSessions from http.ts’s POST /reset path. The audit’s one high-severity security gap.
  2. Strip accountId from client-facing world_state entities (M1) — the director’s join reply currently re-links every player’s opaque p-<hex> wire id to their accountId. The members array stays; entity accountId goes.
  3. XFF parsing (M2) — stop trusting split(',')[0]; use the rightmost untrusted entry, and record the Caddy trusted_proxies invariant in the deploy runbook (verify the live Caddyfile once).
  4. Rate limiter (M3) — evict full/idle buckets periodically; add an overall per-IP cap on login_email alongside the per-identity key; rate-limit create_link_code (currently unlimited QR-generation CPU + inserts).
  5. Message-size limits (M4) — maxPayload on both WebSocketServers; .max() on password, email, create_world.name, set_world_note.note.
  6. scrypt cost actually applied (M5) — pass { N: SCRYPT_N } at hash AND verify time; parse the stored N on verify so parameter upgrades work.
  7. Same-account lifecycle, server halves (M6) — emit a TAKEN_OVER reason on the newest-wins disconnect in world_server.gd (013’s client already handles it, dormant); refcount ActiveWorld.players per connection instead of a Set<accountId> (closing one of two same-account tabs currently shuts the world down under the survivor); convert server.ts’s joinedWorlds Map to the pair set the 010 plan specified (one socket, two sessions currently leaks a refcount).
  8. Timing-based account enumeration (M7) — dummy scrypt on the unknown-email login path; ack request_password_reset before (or without awaiting) the Resend HTTP call.
  9. Server lows from the audit, same pass: don’t echo err.message in INTERNAL_ERROR; fix the grant-mint-failure phantom-player leak (server.ts:342-345); uppercase+trim link-code redemption input; prune world move_budget entries for departed entities; consider movement-budget cap ~40 for honest diagonal headroom (audit low — Kris’s call if behaviour changes).

Definition of done

Each fix lands with a test where the suite can express it (reset-revocation, world_state accountId absence, size-limit rejection, scrypt N round-trip, TAKEN_OVER reason, refcounted players). Full suite green, deploy per the standing runbook, existing verify scripts still pass live. Handoff note + review, per convention.

View source in repository · also available as raw markdown.