From 6aa31e9ebef50df70ec234025df9a18d9ae28305 Mon Sep 17 00:00:00 2001 From: Julio A <52619625+julio-cfa@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:06:20 +0200 Subject: [PATCH 1/4] fix: imported fixes (#33246) --- .changeset/many-rules-shout.md | 5 + .../server/functions/canDeleteMessage.ts | 28 +- .../app/otr/server/methods/updateOTRAck.ts | 37 +- .../tabs/AppDetails/AppDetails.tsx | 11 +- .../tabs/AppReleases/AppReleasesItem.tsx | 4 +- .../views/marketplace/lib/purifyOptions.ts | 50 ++ apps/meteor/server/models/raw/Rooms.ts | 4 + .../meteor/tests/end-to-end/api/24-methods.js | 557 ++++++++++++++++++ packages/gazzodown/package.json | 2 + .../gazzodown/src/emoji/EmojiRenderer.tsx | 11 +- packages/gazzodown/src/katex/KatexBlock.tsx | 1 + packages/gazzodown/src/katex/KatexElement.tsx | 1 + .../model-typings/src/models/IRoomsModel.ts | 1 + 13 files changed, 697 insertions(+), 15 deletions(-) create mode 100644 .changeset/many-rules-shout.md create mode 100644 apps/meteor/client/views/marketplace/lib/purifyOptions.ts diff --git a/.changeset/many-rules-shout.md b/.changeset/many-rules-shout.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/many-rules-shout.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 91769e71270a..3461c35e98b1 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -1,7 +1,8 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; import { getValue } from '../../../settings/server/raw'; +import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; const elapsedTime = (ts: Date): number => { @@ -13,6 +14,25 @@ export const canDeleteMessageAsync = async ( uid: string, { u, rid, ts }: { u: Pick; rid: string; ts: Date }, ): Promise => { + const room = await Rooms.findOneById>(rid, { + projection: { + _id: 1, + ro: 1, + unmuted: 1, + t: 1, + teamId: 1, + prid: 1, + }, + }); + + if (!room) { + return false; + } + + if (!(await canAccessRoomAsync(room, { _id: uid }))) { + return false; + } + const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); if (forceDelete) { @@ -45,12 +65,6 @@ export const canDeleteMessageAsync = async ( } } - const room = await Rooms.findOneById(rid, { projection: { ro: 1, unmuted: 1 } }); - - if (!room) { - return false; - } - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) { // Unless the user was manually unmuted if (u.username && !(room.unmuted || []).includes(u.username)) { diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index 745c70aa4538..6baede8e4e42 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -1,7 +1,10 @@ import type { IOTRMessage } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import notifications from '../../../notifications/server/lib/Notifications'; declare module '@rocket.chat/ui-contexts' { @@ -12,10 +15,40 @@ declare module '@rocket.chat/ui-contexts' { } Meteor.methods({ - updateOTRAck({ message, ack }) { - if (!Meteor.userId()) { + async updateOTRAck({ message, ack }) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); } + + check(ack, String); + check(message, { + _id: String, + rid: String, + msg: String, + t: String, + ts: Date, + u: { + _id: String, + username: String, + name: String, + }, + }); + + if (message?.t !== 'otr') { + throw new Meteor.Error('error-invalid-message', 'Invalid message type', { method: 'updateOTRAck' }); + } + + const room = await Rooms.findOneByIdAndType(message.rid, 'd', { projection: { t: 1, _id: 1, uids: 1 } }); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateOTRAck' }); + } + + if (!(await canAccessRoomAsync(room, { _id: uid })) || (room.uids && (!message.u._id || !room.uids.includes(message.u._id)))) { + throw new Meteor.Error('error-invalid-user', 'Invalid user, not in room', { method: 'updateOTRAck' }); + } + const acknowlegedMessage: IOTRMessage = { ...message, otrAck: ack }; notifications.streamRoomMessage.emit(message.rid, acknowlegedMessage); }, diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx index 8d17f669db83..5f3e427b8391 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx @@ -2,10 +2,12 @@ import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage'; import { ExternalLink } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import React from 'react'; import ScreenshotCarouselAnchor from '../../../components/ScreenshotCarouselAnchor'; import type { AppInfo } from '../../../definitions/AppInfo'; +import { purifyOptions } from '../../../lib/purifyOptions'; import AppDetailsAPIs from './AppDetailsAPIs'; import { normalizeUrl } from './normalizeUrl'; @@ -61,7 +63,14 @@ const AppDetails = ({ app }: AppDetailsProps) => { {t('Description')} - + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx index bc27053ea1d2..974b6d148e61 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx @@ -1,9 +1,11 @@ import { Accordion, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import { useTimeAgo } from '../../../../../hooks/useTimeAgo'; +import { purifyOptions } from '../../../lib/purifyOptions'; type IRelease = { version: string; @@ -36,7 +38,7 @@ const AppReleasesItem = ({ release, ...props }: ReleaseItemProps): ReactElement return ( {release.detailedChangelog?.rendered ? ( - + ) : ( {t('No_release_information_provided')} )} diff --git a/apps/meteor/client/views/marketplace/lib/purifyOptions.ts b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts new file mode 100644 index 000000000000..cef1a2c8c707 --- /dev/null +++ b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts @@ -0,0 +1,50 @@ +export const purifyOptions = { + ALLOWED_TAGS: [ + 'b', + 'i', + 'em', + 'strong', + 'br', + 'p', + 'ul', + 'ol', + 'li', + 'article', + 'aside', + 'figure', + 'section', + 'summary', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hgroup', + 'div', + 'hr', + 'span', + 'wbr', + 'abbr', + 'acronym', + 'cite', + 'code', + 'dfn', + 'figcaption', + 'mark', + 's', + 'samp', + 'sub', + 'sup', + 'var', + 'time', + 'q', + 'del', + 'ins', + 'rp', + 'rt', + 'ruby', + 'bdi', + 'bdo', + ], +}; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index e0466b48c114..6ff896cb70b7 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -900,6 +900,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOne(query, options); } + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions = {}): Promise { + return this.findOne({ _id: roomId, t: type }, options); + } + setCallStatus(_id: IRoom['_id'], status: IRoom['callStatus']): Promise { const query: Filter = { _id, diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index 5a69abfd1a7c..cde7c71be3b9 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -2542,6 +2542,158 @@ describe('Meteor.methods', function () { updateSetting('Message_AllowEditing_BlockEditInMinutes', 0), ]); }); + + describe('message deletion when user is not part of the room', () => { + let ridTestRoom; + let messageIdTestRoom; + let testUser; + let testUserCredentials; + + before('create room, add new owner, and leave room', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + const channelName = `methods-test-channel-${Date.now()}`; + + await request + .post(api('groups.create')) + .set(testUserCredentials) + .send({ + name: channelName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', channelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + ridTestRoom = res.body.group._id; + }); + + await request + .post(methodCall('sendMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [ + { + _id: `${Date.now() + Math.random()}`, + rid: ridTestRoom, + msg: 'just a random test message', + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('result').that.is.an('object'); + messageIdTestRoom = data.result._id; + }); + + await request + .post(methodCall('addUsersToRoom')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [ + { + rid: ridTestRoom, + users: ['rocket.cat'], + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + }); + + await request + .post(api('groups.addOwner')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + userId: 'rocket.cat', + }) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .post(api('groups.leave')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + }); + + it('should not delete a message if the user is no longer member of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + it('should not delete a message if the user was never part of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser)])); + }); }); describe('[@setUserActiveStatus]', () => { @@ -3223,4 +3375,409 @@ describe('Meteor.methods', function () { .end(done); }); }); + + (IS_EE ? describe : describe.skip)('[@auditGetAuditions] EE', () => { + let testUser; + let testUserCredentials; + + const now = new Date(); + const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(); + const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime(); + + before('create test user', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + + before('generate audits data', async () => { + await request + .post(methodCall('auditGetMessages')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'auditGetMessages', + params: [ + { + type: '', + msg: 'test1234', + startDate: { $date: startDate }, + endDate: { $date: endDate }, + rid: 'GENERAL', + users: [], + }, + ], + id: '14', + msg: 'method', + }), + }); + }); + + after(() => Promise.all([deleteUser(testUser)])); + + it('should fail if the user does not have permissions to get auditions', async () => { + await request + .post(methodCall('auditGetAuditions')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'auditGetAuditions', + params: [ + { + startDate: { $date: startDate }, + endDate: { $date: endDate }, + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('error', 'Not allowed'); + }); + }); + + it('should not return more user data than necessary - e.g. passwords, hashes, tokens', async () => { + await request + .post(methodCall('auditGetAuditions')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'auditGetAuditions', + params: [ + { + startDate: { $date: startDate }, + endDate: { $date: endDate }, + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('result').that.is.an('array'); + expect(data.result.length).to.be.greaterThan(0); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', '18'); + data.result.forEach((item) => { + expect(item).to.have.all.keys('_id', 'ts', 'results', 'u', 'fields', '_updatedAt'); + expect(item.u).to.not.have.property('services'); + expect(item.u).to.not.have.property('roles'); + expect(item.u).to.not.have.property('lastLogin'); + expect(item.u).to.not.have.property('statusConnection'); + expect(item.u).to.not.have.property('emails'); + }); + }); + }); + }); + + describe('UpdateOTRAck', () => { + let testUser; + let testUser2; + let testUserCredentials; + let dmTestId; + + before(async () => { + testUser = await createUser(); + testUser2 = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + + before('create direct conversation between both users', (done) => { + void request + .post(methodCall('createDirectMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'createDirectMessage', + params: [testUser2.username], + id: 'id', + msg: 'method', + }), + }) + .end((_err, res) => { + const result = JSON.parse(res.body.message); + expect(result.result).to.be.an('object'); + expect(result.result).to.have.property('rid').that.is.an('string'); + + dmTestId = result.result.rid; + done(); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'd', roomId: dmTestId }), deleteUser(testUser), deleteUser(testUser2)])); + + it('should fail if required parameters are not present', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + // rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', "Match error: Missing key 'rid'"); + }); + }); + + it('should fail if required parameters have a different type', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: { $ne: 'test' }, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Match error: Expected string, got object in field rid'); + }); + }); + + it('should fail if "t" is not "otr"', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'notOTR', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid message type [error-invalid-message]'); + }); + }); + + it('should fail if room does not exist', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if room is not a DM', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'GENERAL', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if user is not part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid user, not in room [error-invalid-user]'); + }); + }); + + it('should pass if all parameters are present and user is part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + expect(res.body).to.have.a.property('success', true); + }); + }); + }); }); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 8ff2f5873dcf..f49c2c9c5447 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -77,7 +77,9 @@ "react": "*" }, "dependencies": { + "@types/dompurify": "^3.0.5", "date-fns": "^3.3.1", + "dompurify": "^3.1.6", "highlight.js": "^11.5.1", "react-error-boundary": "^3.1.4" }, diff --git a/packages/gazzodown/src/emoji/EmojiRenderer.tsx b/packages/gazzodown/src/emoji/EmojiRenderer.tsx index 7a4ca5324930..84116361157c 100644 --- a/packages/gazzodown/src/emoji/EmojiRenderer.tsx +++ b/packages/gazzodown/src/emoji/EmojiRenderer.tsx @@ -1,5 +1,6 @@ import { MessageEmoji, ThreadMessageEmoji } from '@rocket.chat/fuselage'; import type * as MessageParser from '@rocket.chat/message-parser'; +import DOMPurify from 'dompurify'; import { ReactElement, useMemo, useContext, memo } from 'react'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; @@ -14,10 +15,12 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): const fallback = useMemo(() => ('unicode' in emoji ? emoji.unicode : `:${emoji.shortCode ?? emoji.value.value}:`), [emoji]); + const sanitizedFallback = DOMPurify.sanitize(fallback); + const descriptors = useMemo(() => { - const detected = detectEmoji?.(fallback); + const detected = detectEmoji?.(sanitizedFallback); return detected?.length !== 0 ? detected : undefined; - }, [detectEmoji, fallback]); + }, [detectEmoji, sanitizedFallback]); return ( <> @@ -34,8 +37,8 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): )} )) ?? ( - - {fallback} + + {sanitizedFallback} )} diff --git a/packages/gazzodown/src/katex/KatexBlock.tsx b/packages/gazzodown/src/katex/KatexBlock.tsx index 25e9c77dc863..f3a03bd668d0 100644 --- a/packages/gazzodown/src/katex/KatexBlock.tsx +++ b/packages/gazzodown/src/katex/KatexBlock.tsx @@ -15,6 +15,7 @@ const KatexBlock = ({ code }: KatexBlockProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/gazzodown/src/katex/KatexElement.tsx b/packages/gazzodown/src/katex/KatexElement.tsx index 3595f698f7ae..099c2f82cf8c 100644 --- a/packages/gazzodown/src/katex/KatexElement.tsx +++ b/packages/gazzodown/src/katex/KatexElement.tsx @@ -15,6 +15,7 @@ const KatexElement = ({ code }: KatexElementProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 173a373c7df8..231dce5cd06d 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -198,6 +198,7 @@ export interface IRoomsModel extends IBaseModel { setE2eKeyId(roomId: string, e2eKeyId: string, options?: FindOptions): Promise; findOneByImportId(importId: string, options?: FindOptions): Promise; findOneByNameAndNotId(name: string, rid: string): Promise; + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options?: FindOptions): Promise; findOneByDisplayName(displayName: string, options?: FindOptions): Promise; findOneByNameAndType( name: string, From c5bd0a81e5aefe8557c3e2273ae06c1ba1f383f5 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 12 Sep 2024 19:24:53 +0200 Subject: [PATCH 2/4] Remove unused test --- .../meteor/tests/end-to-end/api/24-methods.js | 104 ------------------ 1 file changed, 104 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index cde7c71be3b9..a2dfb65b1373 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -3376,110 +3376,6 @@ describe('Meteor.methods', function () { }); }); - (IS_EE ? describe : describe.skip)('[@auditGetAuditions] EE', () => { - let testUser; - let testUserCredentials; - - const now = new Date(); - const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime(); - const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime(); - - before('create test user', async () => { - testUser = await createUser(); - testUserCredentials = await login(testUser.username, password); - }); - - before('generate audits data', async () => { - await request - .post(methodCall('auditGetMessages')) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'auditGetMessages', - params: [ - { - type: '', - msg: 'test1234', - startDate: { $date: startDate }, - endDate: { $date: endDate }, - rid: 'GENERAL', - users: [], - }, - ], - id: '14', - msg: 'method', - }), - }); - }); - - after(() => Promise.all([deleteUser(testUser)])); - - it('should fail if the user does not have permissions to get auditions', async () => { - await request - .post(methodCall('auditGetAuditions')) - .set(testUserCredentials) - .send({ - message: JSON.stringify({ - method: 'auditGetAuditions', - params: [ - { - startDate: { $date: startDate }, - endDate: { $date: endDate }, - }, - ], - id: '18', - msg: 'method', - }), - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.a.property('message'); - const data = JSON.parse(res.body.message); - expect(data).to.have.a.property('error'); - expect(data.error).to.have.a.property('error', 'Not allowed'); - }); - }); - - it('should not return more user data than necessary - e.g. passwords, hashes, tokens', async () => { - await request - .post(methodCall('auditGetAuditions')) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'auditGetAuditions', - params: [ - { - startDate: { $date: startDate }, - endDate: { $date: endDate }, - }, - ], - id: '18', - msg: 'method', - }), - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.a.property('success', true); - expect(res.body).to.have.a.property('message').that.is.a('string'); - const data = JSON.parse(res.body.message); - expect(data).to.have.a.property('result').that.is.an('array'); - expect(data.result.length).to.be.greaterThan(0); - expect(data).to.have.a.property('msg', 'result'); - expect(data).to.have.a.property('id', '18'); - data.result.forEach((item) => { - expect(item).to.have.all.keys('_id', 'ts', 'results', 'u', 'fields', '_updatedAt'); - expect(item.u).to.not.have.property('services'); - expect(item.u).to.not.have.property('roles'); - expect(item.u).to.not.have.property('lastLogin'); - expect(item.u).to.not.have.property('statusConnection'); - expect(item.u).to.not.have.property('emails'); - }); - }); - }); - }); - describe('UpdateOTRAck', () => { let testUser; let testUser2; From ff05ce2b52754386eb1f350dd0579f3d40648802 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 12 Sep 2024 20:32:19 +0200 Subject: [PATCH 3/4] Fix tests --- apps/meteor/tests/end-to-end/api/24-methods.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index a2dfb65b1373..fcfd7fac2c43 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -2550,6 +2550,8 @@ describe('Meteor.methods', function () { let testUserCredentials; before('create room, add new owner, and leave room', async () => { + await updatePermission('leave-p', ['admin', 'user']); + await updatePermission('leave-c', ['admin', 'user']); testUser = await createUser(); testUserCredentials = await login(testUser.username, password); const channelName = `methods-test-channel-${Date.now()}`; @@ -2692,7 +2694,14 @@ describe('Meteor.methods', function () { }); }); - after(() => Promise.all([deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser)])); + after(() => + Promise.all([ + deleteRoom({ type: 'p', roomId: ridTestRoom }), + deleteUser(testUser), + updatePermission('leave-p', ['admin', 'user']), + updatePermission('leave-c', ['admin', 'user']), + ]), + ); }); }); From 77f595dbed04fc53ca3cb3529ff5c704c51ad974 Mon Sep 17 00:00:00 2001 From: Julio Date: Thu, 12 Sep 2024 20:33:48 +0200 Subject: [PATCH 4/4] Fix tests --- apps/meteor/tests/end-to-end/api/24-methods.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index fcfd7fac2c43..07e28e60951e 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -2698,8 +2698,8 @@ describe('Meteor.methods', function () { Promise.all([ deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser), - updatePermission('leave-p', ['admin', 'user']), - updatePermission('leave-c', ['admin', 'user']), + updatePermission('leave-p', ['admin']), + updatePermission('leave-c', ['admin']), ]), ); });