From e14fa89450540f0450877d30582703018f9d879c Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Wed, 16 Oct 2024 01:17:31 +0200 Subject: [PATCH] fix: imported fixes (#33583) --- .changeset/selfish-schools-leave.md | 5 ++ apps/meteor/app/api/server/v1/settings.ts | 6 ++ .../server/methods/createDiscussion.ts | 3 +- .../server/methods/sendFileMessage.ts | 3 +- .../integrations/server/lib/triggerHandler.js | 22 ++++-- .../server/functions/disableCustomScripts.ts | 14 ++++ .../app/lib/server/methods/createChannel.ts | 2 +- .../lib/server/methods/createPrivateGroup.ts | 2 +- .../app/lib/server/methods/saveSetting.ts | 9 +++ .../app/lib/server/methods/saveSettings.ts | 6 ++ .../functions/disableCustomScripts.tests.ts | 69 +++++++++++++++++++ 11 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 .changeset/selfish-schools-leave.md create mode 100644 apps/meteor/app/lib/server/functions/disableCustomScripts.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/disableCustomScripts.tests.ts diff --git a/.changeset/selfish-schools-leave.md b/.changeset/selfish-schools-leave.md new file mode 100644 index 00000000000..eacb88108a0 --- /dev/null +++ b/.changeset/selfish-schools-leave.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/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 574f4ee6419..94b25120295 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -13,6 +13,7 @@ import type { FindOptions } from 'mongodb'; import _ from 'underscore'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { disableCustomScripts } from '../../../lib/server/functions/disableCustomScripts'; import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; @@ -173,6 +174,11 @@ API.v1.addRoute( throw new Meteor.Error('error-id-param-not-provided', 'The parameter "id" is required'); } + // Disable custom scripts in cloud trials to prevent phishing campaigns + if (disableCustomScripts() && /^Custom_Script_/.test(this.urlParams._id)) { + return API.v1.unauthorized(); + } + // allow special handling of particular setting types const setting = await Settings.findOneNotHiddenById(this.urlParams._id); diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index 96e0bd84639..f2b4533489d 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -223,7 +223,8 @@ export const createDiscussion = async ( if (!(await hasAtLeastOnePermissionAsync(userId, ['start-discussion', 'start-discussion-other-user'], prid))) { throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' }); } - const user = await Users.findOneById(userId); + const user = await Users.findOneById(userId, { projection: { services: 0 } }); + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createDiscussion', diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 73dfd0216a7..8ed534c7015 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -167,7 +167,8 @@ export const sendFileMessage = async ( parseAttachmentsForE2EE: true, }, ): Promise<boolean> => { - const user = await Users.findOneById(userId); + const user = await Users.findOneById(userId, { projection: { services: 0 } }); + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage', diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 7a2992f9151..a9e079a8382 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -180,6 +180,20 @@ class RocketChatIntegrationHandler { } mapEventArgsToData(data, { event, message, room, owner, user }) { + /* The "services" field contains sensitive information such as + the user's password hash. To prevent this information from being + sent to the webhook, we're checking and removing it by destructuring + the user and owner objects while discarding the "services" field. + */ + + const omitServicesField = (object) => { + const { services, ...objectWithoutServicesField } = object; + return objectWithoutServicesField; + }; + + const userWithoutServicesField = user?.services ? omitServicesField(user) : user; + const ownerWithoutServicesField = owner?.services ? omitServicesField(owner) : owner; + switch (event) { case 'sendMessage': data.channel_id = room._id; @@ -215,7 +229,7 @@ class RocketChatIntegrationHandler { data.user_id = message.u._id; data.user_name = message.u.username; data.text = message.msg; - data.user = user; + data.user = userWithoutServicesField; data.room = room; data.message = message; @@ -233,7 +247,7 @@ class RocketChatIntegrationHandler { data.timestamp = room.ts; data.user_id = owner._id; data.user_name = owner.username; - data.owner = owner; + data.owner = ownerWithoutServicesField; data.room = room; break; case 'roomArchived': @@ -244,7 +258,7 @@ class RocketChatIntegrationHandler { data.channel_name = room.name; data.user_id = user._id; data.user_name = user.username; - data.user = user; + data.user = userWithoutServicesField; data.room = room; if (user.type === 'bot') { @@ -255,7 +269,7 @@ class RocketChatIntegrationHandler { data.timestamp = user.createdAt; data.user_id = user._id; data.user_name = user.username; - data.user = user; + data.user = userWithoutServicesField; if (user.type === 'bot') { data.bot = true; diff --git a/apps/meteor/app/lib/server/functions/disableCustomScripts.ts b/apps/meteor/app/lib/server/functions/disableCustomScripts.ts new file mode 100644 index 00000000000..0d1f1a2b320 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/disableCustomScripts.ts @@ -0,0 +1,14 @@ +import { License } from '@rocket.chat/license'; + +export const disableCustomScripts = () => { + const license = License.getLicense(); + + if (!license) { + return false; + } + + const isCustomScriptDisabled = process.env.DISABLE_CUSTOM_SCRIPTS === 'true'; + const isTrialLicense = license?.information.trial; + + return isCustomScriptDisabled && isTrialLicense; +}; diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index a490fa1e3c4..8903583fb24 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -35,7 +35,7 @@ export const createChannelMethod = async ( throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } - const user = await Users.findOneById(userId); + const user = await Users.findOneById(userId, { projection: { services: 0 } }); if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index 3b92b260782..1efa98948eb 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -56,7 +56,7 @@ Meteor.methods<ServerMethods>({ }); } - const user = await Users.findOneById(uid); + const user = await Users.findOneById(uid, { projection: { services: 0 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createPrivateGroup', diff --git a/apps/meteor/app/lib/server/methods/saveSetting.ts b/apps/meteor/app/lib/server/methods/saveSetting.ts index 4f4d29b9fa2..61f5fbbd34c 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.ts +++ b/apps/meteor/app/lib/server/methods/saveSetting.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { disableCustomScripts } from '../functions/disableCustomScripts'; import { notifyOnSettingChanged } from '../lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -39,6 +40,14 @@ Meteor.methods<ServerMethods>({ // Verify the _id passed in is a string. check(_id, String); + // Disable custom scripts in cloud trials to prevent phishing campaigns + if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { + throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { + method: 'saveSetting', + settingId: _id, + }); + } + const setting = await Settings.findOneById(_id); // Verify the value is what it should be diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts index dffa986b721..7a18bbc808d 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ b/apps/meteor/app/lib/server/methods/saveSettings.ts @@ -9,6 +9,7 @@ import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { getSettingPermissionId } from '../../../authorization/lib'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; +import { disableCustomScripts } from '../functions/disableCustomScripts'; import { notifyOnSettingChangedById } from '../lib/notifyListener'; declare module '@rocket.chat/ddp-client' { @@ -73,6 +74,11 @@ Meteor.methods<ServerMethods>({ return settingsNotAllowed.push(_id); } + // Disable custom scripts in cloud trials to prevent phishing campaigns + if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { + return settingsNotAllowed.push(_id); + } + const setting = await Settings.findOneById(_id); // Verify the value is what it should be switch (setting?.type) { diff --git a/apps/meteor/tests/unit/app/lib/server/functions/disableCustomScripts.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/disableCustomScripts.tests.ts new file mode 100644 index 00000000000..a06abe7ac27 --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/disableCustomScripts.tests.ts @@ -0,0 +1,69 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +describe('disableCustomScripts', () => { + let mockLicense: sinon.SinonStubbedInstance<any>; + let disableCustomScripts: () => boolean; + let disableCustomScriptsVar: any; + + beforeEach(() => { + disableCustomScriptsVar = process.env.DISABLE_CUSTOM_SCRIPTS; + mockLicense = { + getLicense: sinon.stub(), + }; + + disableCustomScripts = proxyquire('../../../../../../app/lib/server/functions/disableCustomScripts.ts', { + '@rocket.chat/license': { License: mockLicense }, + }).disableCustomScripts; + }); + + afterEach(() => { + process.env.DISABLE_CUSTOM_SCRIPTS = disableCustomScriptsVar; + sinon.restore(); + }); + + it('should return false when license is missing', () => { + mockLicense.getLicense.returns(null); + + const result = disableCustomScripts(); + expect(result).to.be.false; + }); + + it('should return false when DISABLE_CUSTOM_SCRIPTS is not true', () => { + mockLicense.getLicense.returns({ + information: { + trial: true, + }, + }); + + const result = disableCustomScripts(); + expect(result).to.be.false; + }); + + it('should return false when license is not a trial', () => { + mockLicense.getLicense.returns({ + information: { + trial: false, + }, + }); + + process.env.DISABLE_CUSTOM_SCRIPTS = 'true'; + + const result = disableCustomScripts(); + expect(result).to.be.false; + }); + + it('should return true when DISABLE_CUSTOM_SCRIPTS is true and license is a trial', () => { + mockLicense.getLicense.returns({ + information: { + trial: true, + }, + }); + + process.env.DISABLE_CUSTOM_SCRIPTS = 'true'; + + const result = disableCustomScripts(); + expect(result).to.be.true; + }); +}); -- GitLab