Skip to content

Authorization check missing in mergeSpecifiedPersons and #10914

Description

@johnweeklys

Security Fix: Authorization check missing in mergeSpecifiedPersons and canMergeSpecifiedPersons

Summary

mergeSpecifiedPersons and canMergeSpecifiedPersons endpoints only validate that the caller's token is well-formed, but do not verify that the caller is one of the persons being merged. Any authenticated user can merge any two persons by knowing their UUIDs.

Affected file

server/account/src/operations.ts


Vulnerable code

canMergeSpecifiedPersons

export async function canMergeSpecifiedPersons (
  ...
): Promise<boolean> {
  decodeTokenVerbose(ctx, token)  // ← only checks token is valid, no ownership check

  const { primaryPerson, secondaryPerson } = params
  ...
}

mergeSpecifiedPersons

export async function mergeSpecifiedPersons (
  ...
): Promise<void> {
  decodeTokenVerbose(ctx, token)  // ← only checks token is valid, no ownership check

  const { primaryPerson, secondaryPerson } = params
  ...
  await doMergePersons(db, primaryPerson, secondaryPerson)
}

Fix

The caller's account (extracted from the token) must be either primaryPerson or secondaryPerson. Otherwise throw Forbidden.

canMergeSpecifiedPersons

export async function canMergeSpecifiedPersons (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: {
    primaryPerson: PersonUuid
    secondaryPerson: PersonUuid
  }
): Promise<boolean> {
  const { account } = decodeTokenVerbose(ctx, token)

  const { primaryPerson, secondaryPerson } = params
  if (primaryPerson == null || primaryPerson === '' || secondaryPerson == null || secondaryPerson === '') {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
  }

  // Caller must be one of the persons being merged
  if (account !== primaryPerson && account !== secondaryPerson) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

  if (primaryPerson === secondaryPerson) {
    return false
  }

  const primaryPersonObj = await db.person.findOne({ uuid: primaryPerson })
  if (primaryPersonObj == null) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person: primaryPerson }))
  }

  const secondaryPersonObj = await db.person.findOne({ uuid: secondaryPerson })
  if (secondaryPersonObj == null) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.PersonNotFound, { person: secondaryPerson }))
  }

  const verifiedSecondaryIds = await db.socialId.find({ personUuid: secondaryPerson, verifiedOn: { $ne: null } })

  return verifiedSecondaryIds.length === 0
}

mergeSpecifiedPersons

export async function mergeSpecifiedPersons (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: {
    primaryPerson: PersonUuid
    secondaryPerson: PersonUuid
  }
): Promise<void> {
  const { account } = decodeTokenVerbose(ctx, token)

  const { primaryPerson, secondaryPerson } = params
  if (primaryPerson == null || primaryPerson === '' || secondaryPerson == null || secondaryPerson === '') {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
  }

  // Caller must be one of the persons being merged
  if (account !== primaryPerson && account !== secondaryPerson) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

  await doMergePersons(db, primaryPerson, secondaryPerson)
}

Impact

Without this fix, any authenticated user can:

  1. Obtain a valid token (e.g. by signing up)
  2. Know or enumerate two person UUIDs
  3. Call mergeSpecifiedPersons to merge those persons — effectively hijacking or destroying another user's account identity

Steps to reproduce

# Attacker is logged in as person A, but merges persons B and C (unrelated to A)
curl -X POST http://<host>:3000/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <attacker_token>" \
  -d '{
    "id": "1",
    "method": "mergeSpecifiedPersons",
    "params": {
      "primaryPerson": "<victim_uuid_1>",
      "secondaryPerson": "<victim_uuid_2>"
    }
  }'

This succeeds with {"id":"1","result":null} — no error, persons merged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions