diff --git a/.changeset/witty-penguins-rush.md b/.changeset/witty-penguins-rush.md new file mode 100644 index 000000000000..632026d6fe2e --- /dev/null +++ b/.changeset/witty-penguins-rush.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 08e1ef17e348..b8ba95ad6104 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -62,6 +62,7 @@ interface IAPIDefaultFieldsToExclude { statusDefault: number; _updatedAt: number; settings: number; + inviteToken: number; } type RateLimiterOptions = { @@ -148,6 +149,7 @@ export class APIClass extends Restivus { public limitedUserFieldsToExcludeIfIsPrivilegedUser: { services: number; + inviteToken: number; }; constructor(properties: IAPIProperties) { @@ -175,10 +177,12 @@ export class APIClass extends Restivus { statusDefault: 0, _updatedAt: 0, settings: 0, + inviteToken: 0, }; this.limitedUserFieldsToExclude = this.defaultLimitedUserFieldsToExclude; this.limitedUserFieldsToExcludeIfIsPrivilegedUser = { services: 0, + inviteToken: 0, }; } diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 1dcf54e403a6..97c92eeb530f 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -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'); } diff --git a/apps/meteor/app/livechat/server/methods/loadHistory.ts b/apps/meteor/app/livechat/server/methods/loadHistory.ts index 8d747cad20d8..373ede1a3610 100644 --- a/apps/meteor/app/livechat/server/methods/loadHistory.ts +++ b/apps/meteor/app/livechat/server/methods/loadHistory.ts @@ -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'; @@ -23,9 +24,11 @@ Meteor.methods({ 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 } }); diff --git a/apps/meteor/app/livechat/server/methods/loginByToken.ts b/apps/meteor/app/livechat/server/methods/loginByToken.ts index 54ba97e89926..cae23e6d16f7 100644 --- a/apps/meteor/app/livechat/server/methods/loginByToken.ts +++ b/apps/meteor/app/livechat/server/methods/loginByToken.ts @@ -14,6 +14,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ 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) { diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 5376bd6ae64b..bac4349ec72c 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -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'; @@ -55,6 +56,14 @@ function inviteAll(type: T): SlashCommand['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 }, }); diff --git a/apps/meteor/server/models/raw/Team.ts b/apps/meteor/server/models/raw/Team.ts index c5c8b5f286d2..5f4b6765eb20 100644 --- a/apps/meteor/server/models/raw/Team.ts +++ b/apps/meteor/server/models/raw/Team.ts @@ -45,10 +45,10 @@ export class TeamRaw extends BaseRaw implements ITeamModel { query?: Filter, ): FindCursor

| FindCursor { 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( @@ -57,10 +57,10 @@ export class TeamRaw extends BaseRaw implements ITeamModel { query?: Filter, ): FindPaginated> { 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, type: TEAM_TYPE): FindCursor; diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js index b311af16e764..92eae0474e20 100644 --- a/apps/meteor/tests/data/api-data.js +++ b/apps/meteor/tests/data/api-data.js @@ -48,6 +48,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({ diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index 2f2984a2699a..2645e0f42f21 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -636,6 +636,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}`; @@ -694,18 +698,49 @@ 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); }); - after(async () => { - await deleteUser(deactivatedUser); - await deleteUser(user); - await deleteUser(user2); - user2 = undefined; + before('Create a group', async () => { + group = ( + await createRoom({ + type: 'p', + name: `group.test.${Date.now()}-${Math.random()}`, + }) + ).body.group; + }); - await updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']); - await updateSetting('API_Apply_permission_view-outside-room_on_users-list', false); + 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), + ]), + ); + it('should query all users in the system', (done) => { request .get(api('users.list')) @@ -823,6 +858,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('[/users.setAvatar]', () => { diff --git a/apps/meteor/tests/end-to-end/api/16-commands.js b/apps/meteor/tests/end-to-end/api/16-commands.js index 5022feea1cd7..9fb8528b40b4 100644 --- a/apps/meteor/tests/end-to-end/api/16-commands.js +++ b/apps/meteor/tests/end-to-end/api/16-commands.js @@ -1,9 +1,10 @@ +import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper.js'; -import { createRoom } from '../../data/rooms.helper.js'; +import { createRoom, deleteRoom } from '../../data/rooms.helper.js'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper.js'; @@ -391,4 +392,133 @@ describe('[Commands]', function () { }); }); }); + + describe('Command "invite-all-from"', function () { + let group; + let group1; + let channel; + let user1; + let user2; + let user1Credentials; + let user2Credentials; + + this.beforeAll(async () => { + user1 = await createUser(); + user2 = await createUser(); + + [user1Credentials, user2Credentials] = await Promise.all([login(user1.username, password), login(user2.username, password)]); + }); + + this.beforeAll(async () => { + const [response1, response2, response3] = await Promise.all([ + createRoom({ type: 'p', name: `room1-${Date.now()}.${Random.id()}`, credentials: user1Credentials }), + createRoom({ type: 'c', name: `room2-${Date.now()}.${Random.id()}`, credentials: user2Credentials }), + createRoom({ type: 'p', name: `room3-${Date.now()}.${Random.id()}` }), + ]); + group = response1.body.group; + channel = response2.body.channel; + group1 = response3.body.group; + }); + + this.afterAll(async () => { + await Promise.all([ + deleteRoom({ type: 'p', roomId: group._id }), + deleteRoom({ type: 'c', roomId: channel._id }), + deleteRoom({ type: 'p', roomId: group1._id }), + ]); + await Promise.all([deleteUser(user1), deleteUser(user2)]); + }); + + it('should not add users from group which is not accessible by current user', async () => { + await request + .post(api('commands.run')) + .set(user2Credentials) + .send({ + roomId: channel._id, + command: 'invite-all-from', + params: `#${group.name}`, + msg: { + _id: Random.id(), + rid: channel._id, + msg: `invite-all-from #${group.name}`, + }, + triggerId: Random.id(), + }) + .expect(200) + .expect(async (res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .get(api('channels.members')) + .query({ roomId: channel._id }) + .set(user2Credentials) + .expect(200) + .expect((res) => { + const isUser1Added = res.body.members.some((member) => member.username === user1.username); + expect(isUser1Added).to.be.false; + }); + }); + + it('should not add users to a room that is not accessible by the current user', async () => { + await request + .post(api('commands.run')) + .set(user1Credentials) + .send({ + roomId: group1._id, + command: 'invite-all-from', + params: `#${group.name}`, + msg: { + _id: Random.id(), + rid: group1._id, + msg: `invite-all-from #${group.name}`, + }, + triggerId: Random.id(), + }) + .expect(403) + .expect((res) => { + expect(res.body).to.have.a.property('error', 'unauthorized'); + }); + }); + + it('should add users from group which is accessible by current user', async () => { + await request + .post(api('groups.invite')) + .set(user1Credentials) + .send({ + roomId: group._id, + userId: user2._id, + }) + .expect(200); + + await request + .post(api('commands.run')) + .set(user2Credentials) + .send({ + roomId: channel._id, + command: 'invite-all-from', + params: `#${group.name}`, + msg: { + _id: Random.id(), + rid: channel._id, + msg: `invite-all-from #${group.name}`, + }, + triggerId: Random.id(), + }) + .expect(200) + .expect(async (res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .get(api('channels.members')) + .set(user2Credentials) + .query({ roomId: channel._id }) + .expect(200) + .expect((res) => { + const isUser1Added = res.body.members.some((member) => member.username === user1.username); + expect(isUser1Added).to.be.true; + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/25-teams.js b/apps/meteor/tests/end-to-end/api/25-teams.js index a44fd2a0f6dc..0fbd367bdd5c 100644 --- a/apps/meteor/tests/end-to-end/api/25-teams.js +++ b/apps/meteor/tests/end-to-end/api/25-teams.js @@ -1,10 +1,16 @@ import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; import { updatePermission } from '../../data/permissions.helper'; import { adminUsername, password } from '../../data/user'; -import { createUser, login } from '../../data/users.helper'; +import { createUser, login, deleteUser } from '../../data/users.helper'; + +const deleteTeam = async (credentials, teamName) => { + await request.post(api('teams.delete')).set(credentials).send({ + teamName, + }); +}; describe('[Teams]', () => { before((done) => getCredentials(done)); @@ -20,6 +26,8 @@ describe('[Teams]', () => { let testUser2; const testUserCredentials = {}; + before(() => updatePermission('create-team', ['admin', 'user'])); + before('Create test users', (done) => { let username = `user.test.${Date.now()}`; let email = `${username}@rocket.chat`; @@ -498,17 +506,53 @@ describe('[Teams]', () => { }); describe('/teams.list', () => { - before('Create test team', (done) => { - const teamName = `test-team-${Date.now()}`; - request - .post(api('teams.create')) - .set(credentials) - .send({ - name: teamName, + const teamName = `test-team-list-${Date.now()}`; + let testUser1; + let testUser1Credentials; + let testTeamAdmin; + let testTeam1; + + before('Create test users', async () => { + testUser1 = await createUser(); + }); + + before('login test users', async () => { + testUser1Credentials = await login(testUser1.username, password); + }); + + before('Create test team', async () => { + await request.post(api('teams.create')).set(credentials).send({ + name: teamName, + type: 0, + }); + + const team1Name = `test-team-1-${Date.now()}`; + const teamAdminName = `test-team-admin-${Date.now()}`; + + testTeam1 = ( + await request.post(api('teams.create')).set(testUser1Credentials).send({ + name: team1Name, type: 0, }) - .end(done); + ).body.team; + testTeamAdmin = ( + await request.post(api('teams.create')).set(credentials).send({ + name: teamAdminName, + type: 0, + }) + ).body.team; }); + + after(() => + Promise.all([ + deleteTeam(credentials, teamName), + deleteTeam(testUser1Credentials, testTeam1.name), + deleteTeam(credentials, testTeamAdmin.name), + ]), + ); + + after('delete test users', () => deleteUser(testUser1)); + it('should list all teams', (done) => { request .get(api('teams.list')) @@ -521,7 +565,7 @@ describe('[Teams]', () => { expect(res.body).to.have.property('offset', 0); expect(res.body).to.have.property('total'); expect(res.body).to.have.property('teams'); - expect(res.body.teams).to.have.length.greaterThan(1); + expect(res.body.teams.length).to.be.gte(1); expect(res.body.teams[0]).to.include.property('_id'); expect(res.body.teams[0]).to.include.property('_updatedAt'); expect(res.body.teams[0]).to.include.property('name'); @@ -536,6 +580,54 @@ describe('[Teams]', () => { }) .end(done); }); + + it("should prevent users from accessing unrelated teams via 'query' parameter", () => { + return request + .get(api('teams.list')) + .set(testUser1Credentials) + .query({ + query: JSON.stringify({ _id: { $regex: '.*' } }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body.teams.length).to.be.gte(1); + expect(res.body.teams) + .to.be.an('array') + .and.to.satisfy( + (teams) => teams.every((team) => team.createdBy._id === testUser1._id), + `Expected only user's own teams to be returned, but found unowned teams.\n${JSON.stringify( + res.body.teams.filter((team) => team.createdBy._id !== testUser1._id), + null, + 2, + )}`, + ); + }); + }); + + it("should prevent admins from accessing unrelated teams via 'query' parameter", () => { + return request + .get(api('teams.list')) + .set(credentials) + .query({ + query: JSON.stringify({ _id: { $regex: '.*' } }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body.teams.length).to.be.gte(1); + expect(res.body.teams) + .to.be.an('array') + .and.to.satisfy( + (teams) => teams.every((team) => team.createdBy._id === credentials['X-User-Id']), + `Expected only admin's own teams to be returned, but found unowned teams.\n${JSON.stringify( + res.body.teams.filter((team) => team.createdBy._id !== credentials['X-User-Id']), + null, + 2, + )}`, + ); + }); + }); }); describe('/teams.updateMember', () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts index eae12533adec..ead41ffafd41 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/20-messages.ts @@ -1,8 +1,10 @@ import { faker } from '@faker-js/faker'; +import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, it, after } from 'mocha'; -import { getCredentials } from '../../../data/api-data'; +import { api, getCredentials, request } from '../../../data/api-data'; +import { sendSimpleMessage } from '../../../data/chat.helper'; import { sendMessage, startANewLivechatRoomAndTakeIt, @@ -10,18 +12,24 @@ import { createAgent, makeAgentAvailable, uploadFile, + closeOmnichannelRoom, } from '../../../data/livechat/rooms'; +import { removeAgent } from '../../../data/livechat/users'; import { updateSetting } from '../../../data/permissions.helper'; +import { createRoom, deleteRoom } from '../../../data/rooms.helper'; describe('LIVECHAT - messages', () => { + let agent: ILivechatAgent; before((done) => getCredentials(done)); before(async () => { - await createAgent(); + agent = await createAgent(); await makeAgentAvailable(); await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); }); + after(() => Promise.all([updateSetting('Livechat_Routing_Method', 'Auto_Selection'), removeAgent(agent._id)])); + describe('Quote message feature for visitors', () => { it('it should verify if visitor can quote message', async () => { const { @@ -66,4 +74,50 @@ describe('LIVECHAT - messages', () => { expect(imgMessage).to.have.property('file').that.deep.equal(imgMessage?.files?.[0]); }); }); + + describe('Livechat Messages', async () => { + let room: IOmnichannelRoom; + let privateRoom: IRoom; + let visitor: ILivechatVisitor; + + before(async () => { + await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); + + const data = await startANewLivechatRoomAndTakeIt(); + visitor = data.visitor; + room = data.room; + + const response = await createRoom({ type: 'p', name: `private-room-${Math.random()}` } as any); + privateRoom = response.body.group; + }); + + after(() => Promise.all([closeOmnichannelRoom(room._id), deleteRoom({ roomId: privateRoom._id, type: 'p' })])); + + it('should not allow fetching arbitrary messages from another channel', async () => { + const response = await sendSimpleMessage({ roomId: privateRoom._id } as any); + const { message } = response.body; + + await request + .get(api(`livechat/message/${message._id}`)) + .query({ token: visitor.token, rid: room._id }) + .send() + .expect(400) + .expect((res) => { + expect(res.body.error).to.be.equal('invalid-message'); + }); + }); + + it('should allow fetching messages using their _id and roomId', async () => { + const message = await sendMessage(room._id, 'Hello from visitor', visitor.token); + + await request + .get(api(`livechat/message/${message._id}`)) + .query({ token: visitor.token, rid: room._id }) + .send() + .expect(200) + .expect((res) => { + expect(res.body.message._id).to.be.equal(message._id); + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/methods/loadHistory.ts b/apps/meteor/tests/end-to-end/api/livechat/methods/loadHistory.ts new file mode 100644 index 000000000000..f251d7ebe92d --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/methods/loadHistory.ts @@ -0,0 +1,61 @@ +import type { ILivechatAgent } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { before, describe, it, after } from 'mocha'; +import type { Response } from 'supertest'; + +import { getCredentials, request, methodCallAnon, credentials } from '../../../../data/api-data'; +import { createAgent, makeAgentAvailable, sendMessage, startANewLivechatRoomAndTakeIt } from '../../../../data/livechat/rooms'; +import { removeAgent } from '../../../../data/livechat/users'; +import { updateSetting } from '../../../../data/permissions.helper'; +import { adminUsername } from '../../../../data/user'; + +describe('livechat:loadHistory', function () { + this.retries(0); + let agent: ILivechatAgent; + + before((done) => getCredentials(done)); + + before(async () => { + await updateSetting('Livechat_enabled', true); + agent = await createAgent(adminUsername); + await makeAgentAvailable(credentials); + }); + + after('remove agent', async () => { + await removeAgent(agent._id); + }); + + describe('loadHistory', async () => { + it('prevent getting unrelated message history using regex on rid param', async () => { + const { + room: { _id: roomId }, + visitor: { token }, + } = await startANewLivechatRoomAndTakeIt(); + + await sendMessage(roomId, 'Hello from visitor', token); + + await request + .post(methodCallAnon('livechat:loadHistory')) + .send({ + message: JSON.stringify({ + msg: 'method', + id: 'id2', + method: 'livechat:loadHistory', + params: [ + { + token, + rid: { $regex: '.*' }, + }, + ], + }), + }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + const parsedBody = JSON.parse(res.body.message); + expect(parsedBody).to.have.property('error'); + expect(parsedBody).to.not.have.property('result'); + }); + }); + }); +}); diff --git a/apps/meteor/tests/end-to-end/api/livechat/methods/loginByToken.ts b/apps/meteor/tests/end-to-end/api/livechat/methods/loginByToken.ts new file mode 100644 index 000000000000..be6fee9144d8 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/methods/loginByToken.ts @@ -0,0 +1,66 @@ +import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { before, describe, it, after } from 'mocha'; +import type { Response } from 'supertest'; + +import { getCredentials, request, methodCallAnon, credentials } from '../../../../data/api-data'; +import { + closeOmnichannelRoom, + createAgent, + makeAgentAvailable, + sendMessage, + startANewLivechatRoomAndTakeIt, +} from '../../../../data/livechat/rooms'; +import { removeAgent } from '../../../../data/livechat/users'; +import { updateSetting } from '../../../../data/permissions.helper'; +import { adminUsername } from '../../../../data/user'; + +describe('livechat:loginByTokens', function () { + let visitor: ILivechatVisitor; + let agent: ILivechatAgent; + let room: IOmnichannelRoom; + + this.retries(0); + + before((done) => getCredentials(done)); + + before(async () => { + await updateSetting('Livechat_enabled', true); + agent = await createAgent(adminUsername); + await makeAgentAvailable(credentials); + }); + + before('open livechat room', async () => { + const data = await startANewLivechatRoomAndTakeIt(); + visitor = data.visitor; + room = data.room; + await sendMessage(data.room._id, 'Hello from visitor!', visitor.token); + }); + + after('remove agent and close room', async () => { + await closeOmnichannelRoom(room._id); + await removeAgent(agent._id); + }); + + describe('loginByTokens', async () => { + it('prevent getting arbitrary visitor id using regex in params', async () => { + await request + .post(methodCallAnon('livechat:loginByToken')) + .send({ + message: JSON.stringify({ + msg: 'method', + id: 'id1', + method: 'livechat:loginByToken', + params: [{ $regex: `.*` }], + }), + }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + const parsedBody = JSON.parse(res.body.message); + expect(parsedBody).to.have.property('error'); + expect(parsedBody).to.not.have.property('result'); + }); + }); + }); +});