RocketChat / RocketChat/Rocket.Chat
Message-existence oracle in chat.getMessages leaks info across room-access boundary
Nobody has claimed this yet.
- 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:
- 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. - As user B (ordinary authenticated user, not a member of that group), call
POST /api/v1/chat.getMessageswith{"messageIds": ["<that message id>"]}. - Observe
403 {"success":false,"error":"unauthorized"}. - As user B again, call the same endpoint with a made-up, nonexistent id:
{"messageIds": ["totallyFakeMessageIdThatDoesNotExist123"]}. - 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 OKwithmessages: []
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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