Skip to content

🐛 Reject presence whose version is ahead of the doc - #749

Draft
alecgibson wants to merge 1 commit into
mainfrom
reject-future-presence-version
Draft

alecgibson wants to merge 1 commit into
mainfrom
reject-future-presence-version

Conversation

@alecgibson

@alecgibson alecgibson commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #735

At the moment, the server relays a doc presence whose v is ahead of the doc unchanged, which lets one client make every subscriber fetch:

  1. Submit presence with v: 1e9 to a doc with 20 subscribers
  2. Each subscriber calls doc.fetch(), since v > doc.version
  3. The presence stays pending, so every later op fetches again

This change has the Agent pass its tracked doc version through to transformPresenceToLatestVersion() (a new argument before callback), which reads ops from the lower of that and v. The tracker can lag the database but never lead it, so a v past the last op read is ahead of the doc. Only presence with no tracked version still reads the snapshot. We also now require an integer v, so a fractional one can't sneak past. A rejected presence restores the Agent's previously cached request, so the leave it replays on disconnect isn't rejected too.

We deliberately don't bound presence that is far behind: a fetch with v: 0 already makes the server read the whole op history.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@coveralls

coveralls commented Oct 6, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 97.925% (+0.009%) from 97.916% — reject-future-presence-version into main

Comment thread lib/backend.js Outdated
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from 7f7fc81 to ae15459 Compare October 9, 2026 11:09
@alecgibson
alecgibson requested a balanced review from Copilot October 9, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Versionless database operations can bypass rejection, while rejected presence can leak subscriptions and cached state.

2 open findings
What changed in this PR

Rejects invalid or future document-presence versions to prevent repeated client fetch amplification.

Changes:

  • Requires integer operation and presence versions.
  • Validates presence against tracked or fetched document versions.
  • Adds presence-version and deleted-operation tests.
File Description
lib/​agent.js Validates versions and passes tracked versions for presence transformation.
lib/​backend.js Detects and rejects future presence versions.
test/​agent.js Tests fractional operation-version rejection.
test/​client/​presence/​doc-presence.js Tests presence validation and transformation scenarios.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/backend.js
Comment thread lib/agent.js Outdated
Fixes #735

At the moment, the server relays a doc presence whose `v` is ahead of
the doc unchanged, which lets one client make every subscriber fetch:

 1. Submit presence with `v: 1e9` to a doc with 20 subscribers
 2. Each subscriber calls `doc.fetch()`, since `v > doc.version`
 3. The presence stays pending, so every later op fetches again

This change has the `Agent` pass its tracked doc version through to
`transformPresenceToLatestVersion()` (a new argument before `callback`),
which reads ops from the lower of that and `v`. The tracker can lag the
database but never lead it, so a `v` past the last op read is ahead of
the doc. Only presence with no tracked version still reads the
snapshot. We also now require an integer `v`, so a fractional one can't
sneak past. A rejected presence restores the `Agent`'s previously cached
request, so the leave it replays on disconnect isn't rejected too.

We deliberately don't bound presence that is far *behind*: a `fetch`
with `v: 0` already makes the server read the whole op history.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from ae15459 to df41add Compare October 9, 2026 14:43
@alecgibson
alecgibson requested a balanced review from Copilot October 9, 2026 14:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Document-channel validation remains bypassable, and concurrent rejected requests can corrupt cached presence state.

3 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread lib/backend.js

Backend.prototype.transformPresenceToLatestVersion = function(agent, presence, callback) {
Backend.prototype.transformPresenceToLatestVersion = function(agent, presence, knownVersion, callback) {
if (!presence.c || !presence.d) return callback(null, presence);
Comment thread lib/agent.js

if (request.a === ACTIONS.op || request.a === ACTIONS.presence) {
if (request.v != null && (typeof request.v !== 'number' || request.v < 0)) return 'Invalid version';
if (request.v != null && !util.isValidVersion(request.v)) return 'Invalid version';
Comment thread lib/agent.js
Comment on lines +849 to +851
if (error) {
restorePreviousPresenceRequest(requests, presence, previousRequest);
return callback(error);

This branch has not been deployed

No deployments
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.

Presence version is not clamped, amplifying work on server and other clients

3 participants