trails/apps
Ullrich Schäfer 4e2c5f7d6b
fix(planner): require session row before accepting Yjs WebSocket upgrade
Addresses planner-audit #1 (CRITICAL — WebSocket joins unauthenticated)
with the narrow fix. The upgrade handler used to immediately
\`handleUpgrade\` for any \`/sync/<id>\` path, then \`getOrLoadDoc\` *created*
a Y.Doc on demand. An attacker connecting to random sessionIds could:

  - exhaust process memory by spinning up Y.Doc objects for sessions
    that don't exist (no DB row backed them), and
  - resurrect closed/expired sessions by reconnecting (closed rows
    were still cacheable to \`docs\`).

Now: the upgrade handler calls \`getSession(sessionId)\` first
(\`SELECT \\* FROM planner.sessions WHERE id = ? AND closed = false\`).
Missing or closed → \`socket.destroy()\` before handshake. DB unreachable
also closes — fail closed; the client reconnects after backoff.

Costs one DB roundtrip per upgrade. Upgrades are rare vs message
volume, so the overhead is negligible.

Note: we are NOT adding a join token. The planner is anonymous-by-design
(see CLAUDE.md \"sessions are anonymous and ephemeral\") — knowing the
URL still equals membership. The check here only guards against
attacker-supplied sessionIds with no backing row.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:24:16 +02:00
..
journal ci: wire integration tests into the CI E2E job 2026-05-25 23:27:44 +02:00
mobile Add Jest mock for expo-crypto 2026-05-24 11:54:50 +02:00
planner fix(planner): require session row before accepting Yjs WebSocket upgrade 2026-05-26 00:24:16 +02:00