---
id: server-architecture/014
title: Server hardening batch — the 010 audit's security fixes
epic: server-architecture
state: done
priority: 2
blocked_by: []
estimate: M
created: 2026-08-13
updated: 2026-08-13
claimed_by: session-88fa322b-agent
claimed_at: 2026-08-13T01:35:00.000Z
delivers: []
review_artifact: land-of-lor/docs/handoff/historic/014-server-hardening.md
---

## What

The 010 audit ([`../010-accounts-audit.md`](../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.