Skip to content

Bridge attribution: the linkedUserId derive is skipped when liveRelay arrives as a non-boolean #1293

Description

@lilyshen0722

#1290 added a guard so the bridge's attribution identity is derived from the caller instead of accepted from the client. The rejection half works; the derive half is skipped whenever liveRelay arrives as anything other than a literal boolean true, because the check is strict and the schema field is loosely cast.

backend/routes/integrations.ts:402-406 (main e35d89e6):

if (config && 'linkedUserId' in config && String(config.linkedUserId) !== String(req.user?.id)) {
  return res.status(400).json({ message: 'linkedUserId is derived from the authenticated caller and cannot be set' });
}
if (config && config.liveRelay === true) nextConfig.linkedUserId = req.user?.id;

config.liveRelay === true is strict. liveRelay is declared { type: Boolean }, so Mongoose casts 'true' and 1 to true on write. Send either and the relay switches on while linkedUserId keeps whatever it already held.

Measured

Against the existing route harness at e35d89e6, caller user-1, on a row whose stored config.linkedUserId is VICTIM-USER-ID:

body status stored liveRelay stored linkedUserId
{ liveRelay: true, linkedUserId: 'VICTIM-USER-ID' } 400 — —
{ liveRelay: true } 200 true user-1 ✅ derived
{ liveRelay: 'true' } 200 true VICTIM-USER-ID ❌
{ liveRelay: 1 } 200 true VICTIM-USER-ID ❌

And end-to-end through Mongoose (mongodb-memory-server, raw-driver readback) to confirm the string is not merely stored as a string:

stored liveRelay        : true      (typeof "boolean")
stored linkedUserId     : "VICTIM"
findLiveIntegration hit : true

So the bridge activates and relays under the stale identity.

Reachability — narrower than it looks, and worth stating

The 400 means a caller cannot introduce someone else's id. A stale linkedUserId can only be a previous authorized caller's, so exploiting this needs two parties authorized on the same integration — canDeleteIntegration admits instance admins, the pod's creator, and the integration's creator, so pod-creator + integration-creator is the realistic pair.

The correctness defect stands independently of that: the invariant #1290 set out to establish — liveRelay true implies linkedUserId is whoever turned it on — does not hold, and any future caller that sends a form-encoded or JSON-stringified "true" breaks it without malice.

Suggested fix

Key the derive off the coerced result rather than the raw input, so it cannot drift from what Mongoose will store:

if (config && 'liveRelay' in config && Boolean(config.liveRelay)) {
  nextConfig.linkedUserId = req.user?.id;
}

Worth a test asserting { liveRelay: 'true' } derives, since that is the arm that is currently green and wrong.

Not verified

Whether any live integration currently carries a linkedUserId that is not its most recent enabler — I checked the code path, not production data. Nothing on main writes liveRelay outside the new Connectors page, which only sends a real boolean, so the string path likely has no live traffic yet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions