From c68a8d6e0245ce780e0ae4dd6ab854fff5b4ff18 Mon Sep 17 00:00:00 2001 From: julio-rocketchat Date: Tue, 7 Jan 2025 22:30:56 +0100 Subject: [PATCH] fix: security hotfix --------- docs: add changeset Fix bug in createToken.ts Fix lint Fix lint error --- .changeset/breezy-horses-marry.md | 5 + apps/meteor/app/cors/server/cors.ts | 5 + .../app/oauth2-server-config/server/index.ts | 1 - .../server/oauth/default-services.ts | 21 -- .../components/FingerprintChangeModal.tsx | 8 +- .../FingerprintChangeModalConfirmation.tsx | 10 +- apps/meteor/client/components/RawText.tsx | 5 +- .../provider/OmnichannelRoomIconProvider.tsx | 7 +- .../client/components/UrlChangeModal.tsx | 21 +- apps/meteor/client/lib/utils/createToken.ts | 15 +- .../RoomList/SideBarItemTemplateWithData.tsx | 5 +- .../sidebar/footer/SidebarFooterDefault.tsx | 3 +- .../sidebarv2/footer/SidebarFooterDefault.tsx | 3 +- .../preferences/PreferencesMyDataSection.tsx | 7 +- .../views/account/security/EndToEnd.tsx | 5 +- .../AccountTokensTable/AccountTokensTable.tsx | 11 +- .../tokens/AccountTokensTable/AddToken.tsx | 11 +- .../views/admin/integrations/NewBot.tsx | 15 +- .../views/admin/integrations/NewZapier.tsx | 49 ++- .../incoming/IncomingWebhookForm.tsx | 15 +- .../outgoing/OutgoingWebhookForm.tsx | 35 ++- .../outgoing/history/HistoryItem.tsx | 15 +- .../client/views/admin/mailer/MailerPage.tsx | 3 +- .../views/admin/settings/Setting/Setting.tsx | 4 +- .../groups/OAuthGroupPage/OAuthGroupPage.tsx | 3 +- .../views/admin/users/AdminUserForm.tsx | 9 +- .../AdminUserSetRandomPasswordRadios.tsx | 3 +- .../client/views/banners/LegacyBanner.tsx | 5 +- .../composer/EmojiPicker/EmojiElement.tsx | 3 +- .../EmojiPicker/ToneSelector/ToneItem.tsx | 3 +- .../views/e2e/EnterE2EPasswordModal.tsx | 3 +- .../client/views/e2e/SaveE2EPasswordModal.tsx | 3 +- .../tabs/AppLogs/AppLogsItemEntry.tsx | 3 +- .../components/MessageSearchForm.tsx | 5 +- apps/meteor/tests/end-to-end/api/oauthapps.ts | 280 +++++++++++++----- .../lib/accessors/modify/ModifyCreator.ts | 3 +- .../src/server/accessors/LivechatCreator.ts | 4 +- packages/i18n/src/locales/en.i18n.json | 2 + packages/ui-client/package.json | 4 + .../EmojiPicker/EmojiPickerPreview.tsx | 3 +- packages/web-ui-registration/package.json | 4 + .../src/components/LoginTerms.tsx | 8 +- 42 files changed, 442 insertions(+), 185 deletions(-) create mode 100644 .changeset/breezy-horses-marry.md delete mode 100644 apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts diff --git a/.changeset/breezy-horses-marry.md b/.changeset/breezy-horses-marry.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/breezy-horses-marry.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/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 309053014016..effbb712681b 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -4,6 +4,7 @@ import type { UrlWithParsedQuery } from 'url'; import url from 'url'; import { Logger } from '@rocket.chat/logger'; +import { OAuthApps } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import type { StaticFiles } from 'meteor/webapp'; import { WebApp, WebAppInternals } from 'meteor/webapp'; @@ -48,10 +49,13 @@ WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.Serve } if (settings.get('Enable_CSP')) { + const legacyZapierAvailable = Boolean(await OAuthApps.findOneById('zapier')); + // eslint-disable-next-line @typescript-eslint/naming-convention const cdn_prefixes = [ settings.get('CDN_PREFIX'), settings.get('CDN_PREFIX_ALL') ? null : settings.get('CDN_JSCSS_PREFIX'), + legacyZapierAvailable && 'https://cdn.zapier.com', ] .filter(Boolean) .join(' '); @@ -68,6 +72,7 @@ WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.Serve settings.get('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com', settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'), settings.get('GoogleAnalytics_enabled') && 'https://www.google-analytics.com', + legacyZapierAvailable && 'https://zapier.com', ...settings .get('Extra_CSP_Domains') .split(/[ \n\,]/gim) diff --git a/apps/meteor/app/oauth2-server-config/server/index.ts b/apps/meteor/app/oauth2-server-config/server/index.ts index be26bdb2facb..3914cac5eaad 100644 --- a/apps/meteor/app/oauth2-server-config/server/index.ts +++ b/apps/meteor/app/oauth2-server-config/server/index.ts @@ -1,5 +1,4 @@ import './oauth/oauth2-server'; -import './oauth/default-services'; import './admin/functions/addOAuthApp'; import './admin/methods/updateOAuthApp'; import './admin/methods/deleteOAuthApp'; diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts b/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts deleted file mode 100644 index cd2d4e6c862d..000000000000 --- a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OAuthApps } from '@rocket.chat/models'; - -async function run(): Promise { - if (!(await OAuthApps.findOneById('zapier'))) { - await OAuthApps.insertOne({ - _id: 'zapier', - name: 'Zapier', - active: true, - clientId: 'zapier', - clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', - redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', - _createdAt: new Date(), - _createdBy: { - _id: 'system', - username: 'system', - }, - }); - } -} - -void run(); diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index 834f130bb9f1..354d13df06c2 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -26,14 +27,17 @@ const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintCha is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('Unique_ID_change_detected_description'), + __html: DOMPurify.sanitize(t('Unique_ID_change_detected_description')), }} /> diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx index 7b0d93146a59..7d262d792935 100644 --- a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx +++ b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,14 +30,19 @@ const FingerprintChangeModalConfirmation = ({ is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: newWorkspace ? t('Confirm_new_workspace_description') : t('Confirm_configuration_update_description'), + __html: newWorkspace + ? DOMPurify.sanitize(t('Confirm_new_workspace_description')) + : DOMPurify.sanitize(t('Confirm_configuration_update_description')), }} /> diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index a4220371666f..58ae89e4d041 100644 --- a/apps/meteor/client/components/RawText.tsx +++ b/apps/meteor/client/components/RawText.tsx @@ -1,6 +1,9 @@ +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; /** @deprecated */ -const RawText = ({ children }: { children: string }): ReactElement => ; +const RawText = ({ children }: { children: string }): ReactElement => ( + +); export default RawText; diff --git a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx index 57febd38b9e3..305359f1be9b 100644 --- a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx +++ b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx @@ -1,3 +1,4 @@ +import DOMPurify from 'dompurify'; import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; @@ -84,7 +85,11 @@ export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconPro xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink' style={{ display: 'none' }} - dangerouslySetInnerHTML={{ __html: svgIcons.join('') }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(svgIcons.join(''), { + USE_PROFILES: { svg: true, svgFilters: true }, + }), + }} />, document.body, 'custom-icons', diff --git a/apps/meteor/client/components/UrlChangeModal.tsx b/apps/meteor/client/components/UrlChangeModal.tsx index cf1df67adf00..75a5310861c8 100644 --- a/apps/meteor/client/components/UrlChangeModal.tsx +++ b/apps/meteor/client/components/UrlChangeModal.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,18 +20,22 @@ const UrlChangeModal = ({ onConfirm, siteUrl, currentUrl, onClose }: UrlChangeMo is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', { - postProcess: 'sprintf', - sprintf: [t('Site_Url'), siteUrl, currentUrl], - }), + __html: DOMPurify.sanitize( + t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', { + postProcess: 'sprintf', + sprintf: [t('Site_Url'), siteUrl, currentUrl], + }), + ), }} />

diff --git a/apps/meteor/client/lib/utils/createToken.ts b/apps/meteor/client/lib/utils/createToken.ts index 0795f8103fd1..25360b773ba9 100644 --- a/apps/meteor/client/lib/utils/createToken.ts +++ b/apps/meteor/client/lib/utils/createToken.ts @@ -1 +1,14 @@ -export const createToken = (): string => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +export const createToken = (): string => { + const array = new Uint8Array(16); + if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { + window.crypto.getRandomValues(array); + } else { + // Use Node.js crypto + const { randomBytes } = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires + const buffer = randomBytes(16); + array.set(buffer); + } + return Array.from(array) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +}; diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 4add7090a6db..9e22fe1ea318 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -2,6 +2,7 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings'; import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage'; import { useLayout } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { TFunction } from 'i18next'; import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react'; import { memo, useMemo } from 'react'; @@ -147,7 +148,9 @@ function SideBarItemTemplateWithData({ const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); const message = extended && getMessage(room, lastMessage, t); - const subtitle = message ? : null; + const subtitle = message ? ( + + ) : null; const threadUnread = tunread.length > 0; const variant = diff --git a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx index 66e5150ab2e1..291799c361d3 100644 --- a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx @@ -2,6 +2,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useThemeMode } from '@rocket.chat/ui-theming'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { SidebarFooterWatermark } from './SidebarFooterWatermark'; @@ -32,7 +33,7 @@ const SidebarFooterDefault = (): ReactElement => { width='auto' className={sidebarFooterStyle} dangerouslySetInnerHTML={{ - __html: logo, + __html: DOMPurify.sanitize(logo), }} /> diff --git a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx index 66e5150ab2e1..291799c361d3 100644 --- a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx @@ -2,6 +2,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useThemeMode } from '@rocket.chat/ui-theming'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { SidebarFooterWatermark } from './SidebarFooterWatermark'; @@ -32,7 +33,7 @@ const SidebarFooterDefault = (): ReactElement => { width='auto' className={sidebarFooterStyle} dangerouslySetInnerHTML={{ - __html: logo, + __html: DOMPurify.sanitize(logo), }} /> diff --git a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx index 53e91211a49d..bffd31cda904 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx @@ -1,5 +1,6 @@ import { AccordionItem, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,7 +24,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); @@ -41,7 +42,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); @@ -55,7 +56,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 6eb16d62aea9..c892f4e93cb0 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,6 +1,7 @@ import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button, Divider } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useMethod, useTranslation, useLogout } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { Accounts } from 'meteor/accounts-base'; import type { ComponentProps, ReactElement } from 'react'; import { useCallback, useEffect } from 'react'; @@ -76,7 +77,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { is='p' fontScale='p1' id={e2ePasswordExplanationId} - dangerouslySetInnerHTML={{ __html: t('E2E_Encryption_Password_Explanation') }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Encryption_Password_Explanation')) }} /> @@ -160,7 +161,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { {t('Reset_E2E_Key')} - + diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx index 833b133f0c08..d1c3365b9df1 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx @@ -1,5 +1,6 @@ import { Box, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement, RefObject } from 'react'; import { useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -68,10 +69,12 @@ const AccountTokensTable = (): ReactElement => { , diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index a4d6ef483ca5..fc1d715b5249 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx @@ -1,6 +1,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, TextInput, Button, Margins, Select } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useCallback, useMemo, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -50,10 +51,12 @@ const AddToken = ({ reload }: AddTokenProps) => { setModal(null)} onClose={() => setModal(null)}> , diff --git a/apps/meteor/client/views/admin/integrations/NewBot.tsx b/apps/meteor/client/views/admin/integrations/NewBot.tsx index 2294f9cb9155..efa50938534f 100644 --- a/apps/meteor/client/views/admin/integrations/NewBot.tsx +++ b/apps/meteor/client/views/admin/integrations/NewBot.tsx @@ -1,10 +1,23 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; const NewBot = () => { const { t } = useTranslation(); - return ; + return ( + + ); }; export default NewBot; diff --git a/apps/meteor/client/views/admin/integrations/NewZapier.tsx b/apps/meteor/client/views/admin/integrations/NewZapier.tsx index 565b371f18f6..1842047aa8c5 100644 --- a/apps/meteor/client/views/admin/integrations/NewZapier.tsx +++ b/apps/meteor/client/views/admin/integrations/NewZapier.tsx @@ -2,6 +2,9 @@ import { Box, Skeleton, Margins, Callout } from '@rocket.chat/fuselage'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useOAuthAppQuery } from '../../oauth/hooks/useOAuthAppQuery'; +import PageLoading from '../../root/PageLoading'; + const blogSpotStyleScriptImport = (src: string) => new Promise((resolve) => { const script = document.createElement('script'); @@ -17,6 +20,8 @@ const blogSpotStyleScriptImport = (src: string) => const NewZapier = ({ ...props }) => { const { t } = useTranslation(); + const oauthAppQuery = useOAuthAppQuery('zapier'); + const zapierAvailable = !oauthAppQuery.isLoading && !oauthAppQuery.isError && oauthAppQuery.data; const [script, setScript] = useState(); useEffect(() => { @@ -28,7 +33,7 @@ const NewZapier = ({ ...props }) => { setScript(scriptEl as HTMLScriptElement); }; - if (!script) { + if (!script && zapierAvailable) { importZapier(); } @@ -37,25 +42,39 @@ const NewZapier = ({ ...props }) => { script.parentNode?.removeChild(script); } }; - }, [script]); + }, [script, zapierAvailable]); + + if (oauthAppQuery.isLoading) { + return ; + } return ( <> - - {t('Install_Zapier_from_marketplace')} + + {t(!zapierAvailable ? 'Install_Zapier_from_marketplace_new_workspaces' : 'Install_Zapier_from_marketplace')} - {!script && ( - - - - - - - - - + {zapierAvailable && ( + <> + {!script && ( + + + + + + + + + + )} + + )} - ); }; diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index 9470ec8ca70c..2ccd413fb898 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -19,6 +19,7 @@ import { } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -111,7 +112,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized

-										
+										
 									
@@ -179,10 +180,12 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized {errors?.channel && ( @@ -271,7 +274,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized{t('You_can_use_an_emoji_as_avatar')} diff --git a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx index 3259857f009d..15db3e86f795 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx @@ -18,6 +18,7 @@ import { } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useMemo } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -179,13 +180,18 @@ const OutgoingWebhookForm = () => { - + )} {showTriggerWords && ( @@ -226,10 +232,12 @@ const OutgoingWebhookForm = () => { @@ -359,7 +367,7 @@ const OutgoingWebhookForm = () => { {t('You_can_use_an_emoji_as_avatar')} @@ -435,7 +443,7 @@ const OutgoingWebhookForm = () => {
-										
+										
 									
@@ -485,7 +493,10 @@ const OutgoingWebhookForm = () => { )} /> - +
{event === 'sendMessage' && ( diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx index f016ce36fe7c..d9581600110c 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx @@ -2,6 +2,7 @@ import type { IIntegrationHistory, Serialized } from '@rocket.chat/core-typings' import { Button, Icon, Box, AccordionItem, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; import { outgoingEvents } from '../../../../../../app/integrations/lib/outgoingEvents'; @@ -110,7 +111,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -122,7 +123,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -134,7 +135,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -156,7 +157,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -168,7 +169,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -180,7 +181,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -192,7 +193,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index 9f6942368faa..1a38a80c5d2c 100644 --- a/apps/meteor/client/views/admin/mailer/MailerPage.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerPage.tsx @@ -15,6 +15,7 @@ import { import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import DOMPurify from 'dompurify'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -175,7 +176,7 @@ const MailerPage = () => { {errors.emailBody.message} )} - +
diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 9fb0a5317b05..484eb34cacfa 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -3,6 +3,7 @@ import { isSettingColor, isSetting } from '@rocket.chat/core-typings'; import { Box, Button, Tag } from '@rocket.chat/fuselage'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import { useSettingStructure } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useEffect, useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -108,7 +109,8 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr ); const callout = useMemo( - () => alert && , + () => + alert && , [alert, i18n, t], ); diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx index 50f523571a0e..0bfb6a8101a2 100644 --- a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx @@ -2,6 +2,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { Button } from '@rocket.chat/fuselage'; import { capitalize } from '@rocket.chat/string-helpers'; import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { memo, useEffect, useState } from 'react'; @@ -118,7 +119,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re help={ } diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 1ba9cb3ced5a..fea907c0719c 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -30,6 +30,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import DOMPurify from 'dompurify'; import { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -270,13 +271,15 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD {isVerificationNeeded && !isSmtpEnabled && ( )} {!isVerificationNeeded && ( )} @@ -429,7 +432,7 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD {!isSmtpEnabled && ( )} diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx index 7b06f5930722..aa47bc24b67f 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx @@ -1,5 +1,6 @@ import { Box, FieldHint, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import DOMPurify from 'dompurify'; import type { Control, UseFormSetValue } from 'react-hook-form'; import { Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -58,7 +59,7 @@ const AdminUserSetRandomPasswordRadios = ({ {!isSmtpEnabled && ( diff --git a/apps/meteor/client/views/banners/LegacyBanner.tsx b/apps/meteor/client/views/banners/LegacyBanner.tsx index aaf947d195ae..1227a2a2f831 100644 --- a/apps/meteor/client/views/banners/LegacyBanner.tsx +++ b/apps/meteor/client/views/banners/LegacyBanner.tsx @@ -1,4 +1,5 @@ import { Banner, Icon } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useCallback, useEffect } from 'react'; import type { LegacyBannerPayload } from '../../lib/banners'; @@ -50,7 +51,9 @@ const LegacyBanner = ({ config }: LegacyBannerProps) => { onClose={handleClose} > {typeof text === 'function' ? text() : text} - {html &&
} + {html && ( +
+ )} ); }; diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx index 22c54d7b9eda..e0ae6fad4611 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx @@ -1,5 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { MouseEvent, AllHTMLAttributes } from 'react'; import { memo } from 'react'; @@ -26,7 +27,7 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE } `; - const emojiElement =
; + const emojiElement =
; return ( { let toneEmoji; @@ -23,7 +24,7 @@ const ToneItem = ({ tone }: { tone: number }) => { toneEmoji = ''; } - return ; + return ; }; export default ToneItem; diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx index 8227cd6da79e..888afa1d7b3e 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx @@ -1,5 +1,6 @@ import { Box, PasswordInput, Field, FieldGroup, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import DOMPurify from 'dompurify'; import type { ChangeEvent, ReactElement } from 'react'; import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -48,7 +49,7 @@ const EnterE2EPasswordModal = ({ onClose={onClose} onCancel={onCancel} > - + diff --git a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx index c9e7fa1a8abe..1511b71686d5 100644 --- a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx @@ -1,6 +1,7 @@ import { Box, CodeSnippet } from '@rocket.chat/fuselage'; import { useClipboard } from '@rocket.chat/fuselage-hooks'; import { ExternalLink } from '@rocket.chat/ui-client'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -31,7 +32,7 @@ const SaveE2EPasswordModal = ({ randomPassword, onClose, onCancel, onConfirm }: annotation={t('You_can_do_from_account_preferences')} >

- + {t('Learn_more_about_E2EE')} diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx index 91c18baa01d1..1743fa6013e7 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; import { useHighlightedCode } from '../../../../../hooks/useHighlightedCode'; @@ -22,7 +23,7 @@ const AppLogsItemEntry = ({ severity, timestamp, caller, args }: AppLogsItemEntr

 					
 				
diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx index 1da4c0c4a978..e409df9ee677 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx @@ -2,6 +2,7 @@ import type { IMessageSearchProvider } from '@rocket.chat/core-typings'; import { Box, Field, FieldLabel, FieldHint, Icon, TextInput, ToggleSwitch, Callout } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useEffect } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -57,7 +58,9 @@ const MessageSearchForm = ({ provider, onSearch }: MessageSearchFormProps) => { autoComplete='off' {...register('searchText')} /> - {provider.description && } + {provider.description && ( + + )}
{globalSearchEnabled && ( diff --git a/apps/meteor/tests/end-to-end/api/oauthapps.ts b/apps/meteor/tests/end-to-end/api/oauthapps.ts index 5e42069d9934..39fa94422347 100644 --- a/apps/meteor/tests/end-to-end/api/oauthapps.ts +++ b/apps/meteor/tests/end-to-end/api/oauthapps.ts @@ -50,79 +50,6 @@ describe('[OAuthApps]', () => { }); }); - describe('[/oauth-apps.get]', () => { - before(() => updatePermission('manage-oauth-apps', ['admin'])); - after(() => updatePermission('manage-oauth-apps', ['admin'])); - - it('should return a single oauthApp by id', () => { - return request - .get(api('oauth-apps.get')) - .query({ appId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp).to.have.property('clientSecret'); - }); - }); - it('should return a single oauthApp by client id', () => { - return request - .get(api('oauth-apps.get')) - .query({ clientId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp).to.have.property('clientSecret'); - }); - }); - it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => { - await updatePermission('manage-oauth-apps', []); - await request - .get(api('oauth-apps.get')) - .query({ clientId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp.clientId).to.be.equal('zapier'); - expect(res.body.oauthApp).to.not.have.property('clientSecret'); - }); - }); - it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by appId', async () => { - await updatePermission('manage-oauth-apps', []); - await request - .get(api('oauth-apps.get')) - .query({ appId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp.clientId).to.be.equal('zapier'); - expect(res.body.oauthApp).to.not.have.property('clientSecret'); - }); - }); - it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => { - return request - .get(api('oauth-apps.get')) - .query({ _id: '{ "$ne": "" }' }) - .set(credentials) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'OAuth app not found.'); - }); - }); - }); - describe('[/oauth-apps.create]', () => { it('should return an error when the user does not have the necessary permission', async () => { await updatePermission('manage-oauth-apps', []); @@ -220,6 +147,213 @@ describe('[OAuthApps]', () => { }); }); + describe('[/oauth-apps.get]', () => { + let clientId = ''; + let _id = ''; + let clientSecret = ''; + + before(async () => { + await updatePermission('manage-oauth-apps', ['admin']); + + const res = await request + .post(api('oauth-apps.create')) + .set(credentials) + .send({ + name: `new app ${Date.now()}`, + redirectUri: 'http://localhost:3000', + active: true, + }); + + if (res.statusCode !== 200 || !res.body?.success || !res.body.application?._id || !res.body.application?.clientId) { + console.error(res); + throw new Error('Failed to create oauth app for tests'); + } + + clientId = res.body.application.clientId; + _id = res.body.application._id; + clientSecret = res.body.application.clientSecret; + createdAppsIds.push(_id); + }); + after(() => updatePermission('manage-oauth-apps', ['admin'])); + + it('should return a single oauthApp by client id', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp).to.have.property('clientSecret'); + + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return a single oauthApp by _id', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.have.property('clientSecret'); + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return a single oauthApp by appId (deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.have.property('clientSecret'); + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ clientId }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by _id', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by appId (deprecated)', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ appId: _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid id string is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + + it('should fail returning an oauth app when an invalid clientId is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid clientId string is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + + it('should fail returning an oauth app when an invalid appId is provided (avoid NoSQL injections; deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid appId string is provided (avoid NoSQL injections; deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + }); + describe('[/oauth-apps.update]', () => { let appId: IOAuthApps['_id']; diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts index 00b4640295e5..f4509990edc9 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -17,6 +17,7 @@ import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definitio import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; import * as Messenger from '../../messenger.ts'; +import { randomBytes } from 'node:crypto'; import { BlockBuilder } from '../builders/BlockBuilder.ts'; import { MessageBuilder } from '../builders/MessageBuilder.ts'; @@ -45,7 +46,7 @@ export class ModifyCreator implements IModifyCreator { get: (_target: unknown, prop: string) => { // It's not worthwhile to make an asynchronous request for such a simple method if (prop === 'createToken') { - return () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + return () => randomBytes(16).toString('hex'); } if (prop === 'toJSON') { diff --git a/packages/apps-engine/src/server/accessors/LivechatCreator.ts b/packages/apps-engine/src/server/accessors/LivechatCreator.ts index 0462230b97b0..b0cfbf5be481 100644 --- a/packages/apps-engine/src/server/accessors/LivechatCreator.ts +++ b/packages/apps-engine/src/server/accessors/LivechatCreator.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'crypto'; + import type { ILivechatCreator } from '../../definition/accessors'; import type { IExtraRoomParams } from '../../definition/accessors/ILivechatCreator'; import type { ILivechatRoom } from '../../definition/livechat/ILivechatRoom'; @@ -27,6 +29,6 @@ export class LivechatCreator implements ILivechatCreator { } public createToken(): string { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + return randomBytes(16).toString('hex'); // Ensures 128 bits of entropy } } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 93ea90a09a3c..f376a986ae8a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5513,7 +5513,9 @@ "This_is_a_desktop_notification": "This is a desktop notification", "This_is_a_deprecated_feature_alert": "This is a deprecated feature. It may not work as expected and will not get new updates.", "Zapier_integration_has_been_deprecated": "The Zapier integration has been deprecated, may not work as expected and will not receive updates", + "Zapier_integration_is_not_available": "The Zapier integration has been deprecated and is no longer available for new Rocket.Chat workspaces", "Install_Zapier_from_marketplace": "Install the Zapier app from Marketplace to avoid disruptions", + "Install_Zapier_from_marketplace_new_workspaces": "Install the Zapier app from Marketplace to configure new integrations", "Input": "Input", "This_is_a_push_test_messsage": "This is a push test message", "This_message_was_rejected_by__peer__peer": "This message was rejected by {{peer}} peer.", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a8587428ef52..ef0602ab7e75 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -38,6 +38,7 @@ "@storybook/react": "^8.4.4", "@storybook/react-webpack5": "^8.4.4", "@testing-library/react": "~16.0.1", + "@types/dompurify": "^3.0.5", "@types/jest": "~29.5.14", "@types/react": "~17.0.83", "@types/react-dom": "~17.0.26", @@ -67,5 +68,8 @@ }, "volta": { "extends": "../../package.json" + }, + "dependencies": { + "dompurify": "^3.2.0" } } diff --git a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx index dd6a15a468b0..332f6c831f55 100644 --- a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx +++ b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx @@ -1,5 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { AllHTMLAttributes } from 'react'; const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: string } & Omit, 'is'>) => { @@ -12,7 +13,7 @@ const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: st return ( - + {name} diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 028983ac29fe..720b698ceda9 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -36,6 +36,7 @@ "@storybook/theming": "^8.4.4", "@tanstack/react-query": "patch:@tanstack/react-query@npm%3A5.60.5#~/.yarn/patches/@tanstack-react-query-npm-5.60.5-04c500b172.patch", "@testing-library/react": "~16.0.1", + "@types/dompurify": "^3.0.5", "@types/react": "~17.0.83", "babel-loader": "~9.2.1", "eslint": "~8.45.0", @@ -57,5 +58,8 @@ }, "volta": { "extends": "../../package.json" + }, + "dependencies": { + "dompurify": "^3.2.0" } } diff --git a/packages/web-ui-registration/src/components/LoginTerms.tsx b/packages/web-ui-registration/src/components/LoginTerms.tsx index 67dc916fec2d..d582e0e7321c 100644 --- a/packages/web-ui-registration/src/components/LoginTerms.tsx +++ b/packages/web-ui-registration/src/components/LoginTerms.tsx @@ -1,6 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; import { HorizontalWizardLayoutCaption } from '@rocket.chat/layout'; import { useSetting } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +11,12 @@ export const LoginTerms = (): ReactElement => { return ( - + ); };