Skip to content

fix(collab): authorize the user for the flow on the Yjs websocket - #95

Merged
xiduzo merged 1 commit into
mainfrom
fix/yjs-flow-authorization
Aug 3, 2026
Merged

fix(collab): authorize the user for the flow on the Yjs websocket#95
xiduzo merged 1 commit into
mainfrom
fix/yjs-flow-authorization

Conversation

@xiduzo

@xiduzo xiduzo commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fixes a broken-access-control issue in the realtime collaboration server, reported privately through SECURITY.md by Eesh Saxena, who is credited as co-author.

The problem

GET /yjs/:flowId authenticated the caller but never authorized them for that specific flowId. getOrCreateRoom loaded the document by id alone and persistRoom wrote it back by id alone, with userId used only for awareness and logging. The ownership check the tRPC procedures perform through requireFlowAccess was never on this path, so any authenticated user could read and write any flow's live document.

Related, and fixed here too: viewers were only held read-only on the client (readOnly in use-node-controls, board-target-picker, …). Yjs is bidirectional, so an invited viewer could still write through the socket.

The fix

  • Authorize at the websocket boundary in apps/server/src/index.ts, after getSession and before the room is ever created, by reusing requireFlowAccess — one source of truth for who counts as what. Unauthorized callers get 1008 Forbidden.
  • The resolved role decides write access. handleConnection takes canWrite as a required argument, so no caller can grant write access by omission, and handler.onOpen fails closed if the access decision is missing.
  • Read-only connections may only send sync step 1; step 2 and update messages both write into the doc and are dropped. A connection with no ConnectionInfo is treated as read-only.

Viewers still read normally: the client sends its own step 1 on connect (sync-provider.ts) and the server answers with step 2.

Verification

packages/collab/src/__tests__/yjs-server-access.test.ts — 3 tests covering editor writes landing, viewer writes dropped while reads still work, and unknown connections treated read-only. The db is stubbed with mock.module so no DATABASE_URL is needed. With the guard neutered, 2 of the 3 fail, so they are not vacuous.

  • bun test packages apps/web/src → 277 pass, 0 fail
  • bun run check-types → clean

Note

The fix only reaches users once the server redeploys.

🤖 Generated with Claude Code

The /yjs/:flowId endpoint authenticated the caller but never authorized
them for that flowId: getOrCreateRoom loaded the document by id alone and
persistRoom wrote it back by id alone, so any authenticated user could
both read and write any flow's live document, bypassing the owner and
collaborator model the tRPC procedures enforce via requireFlowAccess.

Authorize at the websocket boundary, before the room is created, reusing
requireFlowAccess so there stays one source of truth for who counts as
what. The resolved role also decides write access: viewers were only held
read-only on the client, and Yjs is bidirectional, so an invited viewer
could still write through the socket. handleConnection now takes canWrite
as a required argument (no default, so no caller grants write by
omission), and read-only connections may only send sync step 1 -- step 2
and update both write into the doc. Viewers still read: the client sends
its own step 1 on connect and the server answers with step 2.

Reported by Eesh Saxena (github.com/eeshsaxena) through SECURITY.md.

Co-authored-by: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xiduzo
xiduzo merged commit c0a56be into main Aug 3, 2026
7 checks passed
@xiduzo
xiduzo deleted the fix/yjs-flow-authorization branch August 3, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant