Skip to content

🔒️ Check the ids and versions in bulk and query requests - #742

Open
shreyanshj10 wants to merge 1 commit into
share:mainfrom
shreyanshj10:fix/issue-736-bulk-id-types
Open

shreyanshj10 wants to merge 1 commit into
share:mainfrom
shreyanshj10:fix/issue-736-bulk-id-types

Conversation

@shreyanshj10

Copy link
Copy Markdown

GHSA-p56x-m2v9-77x3 type-checked c and d on the snapshot fetch actions, but
the ids and versions carried by bulk requests (b) and by a query's
reconnect payload (r) are still handed to the database adapter as they
arrive, so a client can send an object, a number or __proto__ where a document
id is expected, or a non-numeric version.

I pulled the string and version checks already in _checkRequest() out into
isInvalidName() and isInvalidVersion() so the single-doc, bulk and query
paths share one validator, then applied them to:

  • every element of a b array,
  • every key and value of a b version map,
  • the [id, version] pairs in a query's r payload.

A malformed entry now comes back as Invalid id or Invalid version before
fetchBulk(), getOpsBulk() or the query runs, and an r payload that isn't
an array of arrays comes back as Invalid query results.

I deliberately left the single-doc f/s version alone: it is only
type-checked for op and p today, and widening that is a behaviour change
beyond this issue.

Tests

test/agent.js gains a request validation block that sends each malformed
message over a real socket and asserts both the error and that
getSnapshotBulk(), getOpsBulk() and query() were never called. npm test
and npm run lint pass.

Fixes #736

@alecgibson alecgibson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is pretty much what I had in mind for #736, and pulling the checks into isInvalidName() / isInvalidVersion() makes _checkRequest() much easier to read. I see two real gaps, both inline below. b: null still gets through. Single-doc f/s versions are still unchecked, and I'd like to fold those in here. Beyond that, a couple of comment nits.

Nits (personal preference)

  • lib/agent.js L378–380 (and L389–390, L405–406): Do we need these comments? The functions read clearly enough on their own. The bulk and query ones also describe the bug rather than the code: after this change, the ids aren't "handed to the database adapter as they arrive", and they aren't passed "straight through" either. I'd put that why in the commit body, which is empty at the moment, so it's discoverable via git blame and can't drift here.
  • test/agent.js L70–71: (Small one) "the only way" isn't quite true, since Object.defineProperty() does it too. The surprising part is that an object literal would set the prototype instead of creating a key. Maybe say just that?

Generated by Claude Code via /alec-review. Patterns derived from Alec's historical review comments — review with that in mind.

Comment thread lib/agent.js
// Bulk requests carry either an array of ids, or a map of id to version, and
// both are handed to the database adapter as they arrive.
function checkBulkData(data) {
if (typeof data !== 'object') return 'Invalid bulk subscribe data';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

typeof null === 'object', so b: null still passes this check, and for…in over null iterates nothing. With bf, we end up calling getOpsBulk('dogs', null) and replying with a success. With bs, the client gets a raw Cannot convert undefined or null to object from Object.keys() in Backend#subscribeBulk(). Could we add null as a b value to the test table too? At the moment it's only covered as an id or a version.

Suggested change
if (typeof data !== 'object') return 'Invalid bulk subscribe data';
if (!data || typeof data !== 'object') return 'Invalid bulk subscribe data';

Comment thread lib/agent.js
Comment on lines 446 to 448
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 && isInvalidVersion(request.v)) return 'Invalid version';
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we drop the op/p guard and check v on every doc request? Right now {a: 'f', c: 'dogs', d: 'fido', v: {$gt: -1}} (and the s equivalent) passes exactly that to getOps() as from. That's the same shape we now reject in a bulk version map. I know the description calls this out of scope, but rejecting a non-numeric bulk version is the same behaviour change, just through a different action. Real clients only ever send doc.version here, which is a number or null, and nf versions are already validated on the client, so I don't think any legitimate client would notice. (Worth adding f/s cases to the tests too.)

Suggested change
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 && isInvalidVersion(request.v)) return 'Invalid version';
}
if (request.v != null && isInvalidVersion(request.v)) return 'Invalid version';

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 97.819% (+0.03%) from 97.785% — shreyanshj10:fix/issue-736-bulk-id-types into share:main

@shreyanshj10
shreyanshj10 marked this pull request as ready for review October 8, 2026 11:50

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.

Bulk and query-reconnect ids/versions skip the type checks added in GHSA-p56x-m2v9-77x3

3 participants