Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: imported fixes #32875

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-penguins-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)
4 changes: 4 additions & 0 deletions apps/meteor/app/api/server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ interface IAPIDefaultFieldsToExclude {
statusDefault: number;
_updatedAt: number;
settings: number;
inviteToken: number;
}

type RateLimiterOptions = {
Expand Down Expand Up @@ -147,6 +148,7 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {

public limitedUserFieldsToExcludeIfIsPrivilegedUser: {
services: number;
inviteToken: number;
};

constructor(properties: IAPIProperties) {
Expand Down Expand Up @@ -174,10 +176,12 @@ export class APIClass<TBasePath extends string = ''> extends Restivus {
statusDefault: 0,
_updatedAt: 0,
settings: 0,
inviteToken: 0,
};
this.limitedUserFieldsToExclude = this.defaultLimitedUserFieldsToExclude;
this.limitedUserFieldsToExcludeIfIsPrivilegedUser = {
services: 0,
inviteToken: 0,
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ API.v1.addRoute(
throw new Error('invalid-room');
}

let message = await Messages.findOneById(_id);
let message = await Messages.findOneByRoomIdAndMessageId(rid, _id);
if (!message) {
throw new Error('invalid-message');
}
Expand Down
9 changes: 6 additions & 3 deletions apps/meteor/app/livechat/server/methods/loadHistory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { loadMessageHistory } from '../../../lib/server/functions/loadMessageHistory';
Expand All @@ -23,9 +24,11 @@ Meteor.methods<ServerMethods>({
async 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) {
methodDeprecationLogger.method('livechat:loadHistory', '7.0.0');

if (!token || typeof token !== 'string') {
return;
}
check(token, String);
check(rid, String);
check(end, Date);
check(ls, Match.OneOf(String, Date));
check(limit, Number);

const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });

Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/methods/loginByToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods<ServerMethods>({
async 'livechat:loginByToken'(token) {
methodDeprecationLogger.method('livechat:loginByToken', '7.0.0');
check(token, String);
const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });

if (!visitor) {
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/slashcommands-inviteall/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../lib/isTruthy';
import { i18n } from '../../../server/lib/i18n';
import { canAccessRoomAsync } from '../../authorization/server';
import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom';
import { createChannelMethod } from '../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup';
Expand Down Expand Up @@ -55,6 +56,14 @@ function inviteAll<T extends string>(type: T): SlashCommand<T>['callback'] {
});
return;
}

if (!(await canAccessRoomAsync(baseChannel, user))) {
void api.broadcast('notify.ephemeralMessage', userId, message.rid, {
msg: i18n.t('Room_not_exist_or_not_permission', { lng }),
});
return;
}

const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, {
projection: { 'u.username': 1 },
});
Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/server/models/raw/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export class TeamRaw extends BaseRaw<ITeam> implements ITeamModel {
query?: Filter<ITeam>,
): FindCursor<P> | FindCursor<ITeam> {
if (options === undefined) {
return this.find({ _id: { $in: ids }, ...query });
return this.find({ ...query, _id: { $in: ids } });
}

return this.find({ _id: { $in: ids }, ...query }, options);
return this.find({ ...query, _id: { $in: ids } }, options);
}

findByIdsPaginated(
Expand All @@ -57,10 +57,10 @@ export class TeamRaw extends BaseRaw<ITeam> implements ITeamModel {
query?: Filter<ITeam>,
): FindPaginated<FindCursor<ITeam>> {
if (options === undefined) {
return this.findPaginated({ _id: { $in: ids }, ...query });
return this.findPaginated({ ...query, _id: { $in: ids } });
}

return this.findPaginated({ _id: { $in: ids }, ...query }, options);
return this.findPaginated({ ...query, _id: { $in: ids } }, options);
}

findByIdsAndType(ids: Array<string>, type: TEAM_TYPE): FindCursor<ITeam>;
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/tests/data/api-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export function methodCall(methodName) {
return api(`method.call/${methodName}`);
}

export function methodCallAnon(methodName) {
return api(`method.callAnon/${methodName}`);
}

export function log(res) {
console.log(res.req.path);
console.log({
Expand Down
98 changes: 98 additions & 0 deletions apps/meteor/tests/end-to-end/api/01-users.js
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,10 @@ describe('[Users]', function () {
let deactivatedUser;
let user2;
let user2Credentials;
let user3;
let user3Credentials;
let group;
let inviteToken;

before(async () => {
const username = `deactivated_${Date.now()}${apiUsername}`;
Expand Down Expand Up @@ -833,14 +837,44 @@ describe('[Users]', function () {
before(async () => {
user2 = await createUser({ joinDefaultChannels: false });
user2Credentials = await login(user2.username, password);
user3 = await createUser({ joinDefaultChannels: false });
user3Credentials = await login(user3.username, password);
});

before('Create a group', async () => {
group = (
await createRoom({
type: 'p',
name: `group.test.${Date.now()}-${Math.random()}`,
})
).body.group;
});

before('Create invite link', async () => {
inviteToken = (
await request.post(api('findOrCreateInvite')).set(credentials).send({
rid: group._id,
days: 0,
maxUses: 0,
})
).body._id;
});

after('Remove invite link', async () =>
request
.delete(api(`removeInvite/${inviteToken}`))
.set(credentials)
.send(),
);

after(() =>
Promise.all([
clearCustomFields(),
deleteUser(deactivatedUser),
deleteUser(user),
deleteUser(user2),
deleteUser(user3),
deleteRoom({ type: 'p', roomId: group._id }),
updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']),
updateSetting('API_Apply_permission_view-outside-room_on_users-list', false),
]),
Expand Down Expand Up @@ -963,6 +997,70 @@ describe('[Users]', function () {

await request.get(api('users.list')).set(user2Credentials).expect('Content-Type', 'application/json').expect(403);
});

it('should exclude inviteToken in the user item for privileged users even when fields={inviteToken:1} is specified', async () => {
await request
.post(api('useInviteToken'))
.set(user2Credentials)
.send({ token: inviteToken })
.expect(200)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('room');
expect(res.body.room).to.have.property('rid', group._id);
});

await request
.get(api('users.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({
fields: JSON.stringify({ inviteToken: 1 }),
sort: JSON.stringify({ inviteToken: -1 }),
count: 100,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users');
res.body.users.forEach((user) => {
expect(user).to.not.have.property('inviteToken');
});
});
});

it('should exclude inviteToken in the user item for normal users even when fields={inviteToken:1} is specified', async () => {
await updateSetting('API_Apply_permission_view-outside-room_on_users-list', false);
await request
.post(api('useInviteToken'))
.set(user3Credentials)
.send({ token: inviteToken })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('room');
expect(res.body.room).to.have.property('rid', group._id);
});

await request
.get(api('users.list'))
.set(user3Credentials)
.expect('Content-Type', 'application/json')
.query({
fields: JSON.stringify({ inviteToken: 1 }),
sort: JSON.stringify({ inviteToken: -1 }),
count: 100,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users');
res.body.users.forEach((user) => {
expect(user).to.not.have.property('inviteToken');
});
});
});
});

describe('Avatars', () => {
Expand Down
Loading
Loading