RocketChat / RocketChat/Rocket.Chat

Message-existence oracle in chat.getMessages leaks info across room-access boundary

Open
#42,033 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type: bug
Dominant language
TypeScript
Stars
46.1k
Forks
13.9k
Avg merge
3d 3h
Merged PRs (30d)
130

Description

Description:

POST /api/v1/chat.getMessages (added in #41961) is supposed to reject a batch request if any requested message lives in a room the caller can't access. But when none of the requested messageIds match a real, visible message, the permission check is skipped entirely ([].every(Boolean) is vacuously true in JS), so the request "succeeds" with an empty list instead of also being rejected/treated the same way.

This makes the endpoint's response distinguish "message exists, but you can't see it" (403) from "message doesn't exist" (200, empty messages) — letting any authenticated user learn whether a given message ID exists, even inside a private room/channel/DM they've never been a member of. No message content is exposed, only existence.

Code: apps/meteor/server/api/v1/chat.ts, chat.getMessages handler:

const messages = await Messages.findVisibleByIds(messageIds).toArray();

const rids = [...new Set(messages.map(({ rid }) => rid))];
const allowed = await Promise.all(rids.map((rid) => canAccessRoomIdAsync(rid, this.userId)));

// The batch spans rooms, so one unreadable room rejects the whole request.
if (!allowed.every(Boolean)) {
  return API.v1.forbidden();
}
Steps to reproduce:
  1. As user A (any privilege), create a private group user B is not a member of, and post a message in it. Note the message _id.
  2. As user B (ordinary authenticated user, not a member of that group), call POST /api/v1/chat.getMessages with {"messageIds": ["<that message id>"]}.
  3. Observe 403 {"success":false,"error":"unauthorized"}.
  4. As user B again, call the same endpoint with a made-up, nonexistent id: {"messageIds": ["totallyFakeMessageIdThatDoesNotExist123"]}.
  5. Observe 200 {"messages":[],"success":true} — a different, distinguishable response for "doesn't exist" vs. "exists but forbidden".
Expected behavior:

The response should not let the caller distinguish "message exists but is inaccessible" from "message doesn't exist." Messages the caller can't access should just be silently omitted from the result (same as nonexistent ones), rather than causing a different (403) response.

Actual behavior:
  • Real message id in an inaccessible room → 403 Forbidden
  • Nonexistent message id → 200 OK with messages: []

The differing status/body leaks message existence across the room-access boundary.

Server Setup Information:
  • Version of Rocket.Chat Server: 8.9.0-develop (commit 2f18297792, PR #41961)
  • License Type: Community/Starter
  • Number of Users: N/A (local dev)
  • Operating System: Linux (WSL2)
  • Deployment Method: yarn dev (local source checkout)
  • Number of Running Instances: 1
  • DB Replicaset Oplog: N/A
  • NodeJS Version: N/A
  • MongoDB Version: N/A
Client Setup Information
  • Desktop App or Browser Version: N/A (found via direct REST API calls)
  • Operating System: Linux (WSL2)
Additional context

Suggested fix: instead of returning API.v1.forbidden() for the whole batch, filter messages down to only those in rooms the caller can access, and return 200 with that (possibly partial/empty) list in all cases:

const messages = await Messages.findVisibleByIds(messageIds).toArray();
const rids = [...new Set(messages.map(({ rid }) => rid))];
const accessibleRids = new Set(
  (await Promise.all(rids.map(async (rid) => ((await canAccessRoomIdAsync(rid, this.userId)) ? rid : null)))).filter(Boolean),
);
const visibleMessages = messages.filter((m) => accessibleRids.has(m.rid));

return API.v1.success({ messages: await normalizeMessagesForUser(visibleMessages, this.userId) });
Relevant logs:

N/A — behavior reproduced via direct curl calls against the REST API, not through the UI.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/meteor/server/api/v1/chat.ts at the chat.getMessages handler and reproduce the two POST requests described with curl. Ensure inaccessible and nonexistent message IDs produce the same successful response with inaccessible messages omitted, while preserving normalization for accessible messages.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.