diff --git a/.changeset/breezy-horses-marry.md b/.changeset/breezy-horses-marry.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /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 3090530140168..effbb712681b1 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<boolean>('Enable_CSP')) { + const legacyZapierAvailable = Boolean(await OAuthApps.findOneById('zapier')); + // eslint-disable-next-line @typescript-eslint/naming-convention const cdn_prefixes = [ settings.get<string>('CDN_PREFIX'), settings.get<string>('CDN_PREFIX_ALL') ? null : settings.get<string>('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<boolean>('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com', settings.get<boolean>('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'), settings.get<boolean>('GoogleAnalytics_enabled') && 'https://www.google-analytics.com', + legacyZapierAvailable && 'https://zapier.com', ...settings .get<string>('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 be26bdb2facb1..3914cac5eaadc 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 cd2d4e6c862d0..0000000000000 --- 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<void> { - 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 834f130bb9f1a..354d13df06c24 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')), }} /> <Box is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('Unique_ID_change_detected_learn_more_link'), + __html: DOMPurify.sanitize(t('Unique_ID_change_detected_learn_more_link'), { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], + ALLOWED_ATTR: ['href', 'title'], + }), }} /> </GenericModal> diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx index 7b0d93146a599..7d262d7929355 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')), }} /> <Box is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('Unique_ID_change_detected_learn_more_link'), + __html: DOMPurify.sanitize(t('Unique_ID_change_detected_learn_more_link'), { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], + ALLOWED_ATTR: ['href', 'title'], + }), }} /> </GenericModal> diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index a4220371666f4..58ae89e4d041c 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 => <span dangerouslySetInnerHTML={{ __html: children }} />; +const RawText = ({ children }: { children: string }): ReactElement => ( + <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(children) }} /> +); 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 57febd38b9e36..305359f1be9be 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 cf1df67adf00d..75a5310861c8e 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], + }), + ), }} /> <p dangerouslySetInnerHTML={{ - __html: t('Do_you_want_to_change_to_s_question', { - postProcess: 'sprintf', - sprintf: [currentUrl], - }), + __html: DOMPurify.sanitize( + t('Do_you_want_to_change_to_s_question', { + postProcess: 'sprintf', + sprintf: [currentUrl], + }), + ), }} /> </GenericModal> diff --git a/apps/meteor/client/lib/utils/createToken.ts b/apps/meteor/client/lib/utils/createToken.ts index 0795f8103fd18..25360b773ba9c 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 4add7090a6db9..9e22fe1ea318d 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 ? <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }} /> : null; + const subtitle = message ? ( + <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(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 66e5150ab2e14..291799c361d36 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), }} /> <SidebarFooterWatermark /> diff --git a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx index 66e5150ab2e14..291799c361d36 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), }} /> <SidebarFooterWatermark /> diff --git a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx index 53e91211a49d0..bffd31cda9042 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( <MyDataModal title={t('UserDataDownload_Requested')} - text={<Box dangerouslySetInnerHTML={{ __html: text }} />} + text={<Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />} onCancel={() => setModal(null)} />, ); @@ -41,7 +42,7 @@ const PreferencesMyDataSection = () => { setModal( <MyDataModal title={t('UserDataDownload_Requested')} - text={<Box dangerouslySetInnerHTML={{ __html: text }} />} + text={<Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(text) }} />} onCancel={() => setModal(null)} />, ); @@ -55,7 +56,7 @@ const PreferencesMyDataSection = () => { setModal( <MyDataModal title={t('UserDataDownload_Requested')} - text={<Box dangerouslySetInnerHTML={{ __html: text }} />} + text={<Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(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 6eb16d62aea92..c892f4e93cb0a 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<typeof Box>): ReactElement => { is='p' fontScale='p1' id={e2ePasswordExplanationId} - dangerouslySetInnerHTML={{ __html: t('E2E_Encryption_Password_Explanation') }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Encryption_Password_Explanation')) }} /> <Box mbs={36} w='full'> @@ -160,7 +161,7 @@ const EndToEnd = (props: ComponentProps<typeof Box>): ReactElement => { <Box is='h4' fontScale='h4' mbe={12}> {t('Reset_E2E_Key')} </Box> - <Box is='p' fontScale='p1' mbe={12} dangerouslySetInnerHTML={{ __html: t('E2E_Reset_Key_Explanation') }} /> + <Box is='p' fontScale='p1' mbe={12} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Reset_Key_Explanation')) }} /> <Button onClick={handleResetE2eKey} data-qa-type='e2e-encryption-reset-key-button'> {t('Reset_E2E_Key')} </Button> diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx index 833b133f0c087..d1c3365b9df18 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 => { <GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={closeModal}> <Box dangerouslySetInnerHTML={{ - __html: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { - token, - userId, - }), + __html: DOMPurify.sanitize( + t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { + token, + userId, + }), + ), }} /> </GenericModal>, diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index a4d6ef483ca54..fc1d715b5249e 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) => { <GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={() => setModal(null)} onClose={() => setModal(null)}> <Box dangerouslySetInnerHTML={{ - __html: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { - token, - userId, - }), + __html: DOMPurify.sanitize( + t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { + token, + userId, + }), + ), }} /> </GenericModal>, diff --git a/apps/meteor/client/views/admin/integrations/NewBot.tsx b/apps/meteor/client/views/admin/integrations/NewBot.tsx index 2294f9cb91555..efa50938534fb 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 <Box pb={20} fontScale='h4' key='bots' dangerouslySetInnerHTML={{ __html: t('additional_integrations_Bots') }} />; + return ( + <Box + pb={20} + fontScale='h4' + key='bots' + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(t('additional_integrations_Bots'), { + ALLOWED_TAGS: ['a'], + ALLOWED_ATTR: ['href', 'target'], + }), + }} + /> + ); }; export default NewBot; diff --git a/apps/meteor/client/views/admin/integrations/NewZapier.tsx b/apps/meteor/client/views/admin/integrations/NewZapier.tsx index 565b371f18f61..1842047aa8c52 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<HTMLScriptElement>(); 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 <PageLoading />; + } return ( <> - <Callout type='warning' icon='warning' title={t('Zapier_integration_has_been_deprecated')} mbs={16} mbe={4}> - {t('Install_Zapier_from_marketplace')} + <Callout + type='warning' + icon='warning' + title={t(!zapierAvailable ? 'Zapier_integration_is_not_available' : 'Zapier_integration_has_been_deprecated')} + mbs={16} + mbe={4} + > + {t(!zapierAvailable ? 'Install_Zapier_from_marketplace_new_workspaces' : 'Install_Zapier_from_marketplace')} </Callout> - {!script && ( - <Box display='flex' flexDirection='column' alignItems='stretch' mbs={10}> - <Margins blockEnd={14}> - <Skeleton variant='rect' height={71} /> - <Skeleton variant='rect' height={71} /> - <Skeleton variant='rect' height={71} /> - <Skeleton variant='rect' height={71} /> - <Skeleton variant='rect' height={71} /> - </Margins> - </Box> + {zapierAvailable && ( + <> + {!script && ( + <Box display='flex' flexDirection='column' alignItems='stretch' mbs={10}> + <Margins blockEnd={14}> + <Skeleton variant='rect' height={71} /> + <Skeleton variant='rect' height={71} /> + <Skeleton variant='rect' height={71} /> + <Skeleton variant='rect' height={71} /> + <Skeleton variant='rect' height={71} /> + </Margins> + </Box> + )} + <Box id='zapier-goes-here' {...props} /> + </> )} - <Box id='zapier-goes-here' {...props} /> </> ); }; diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index 9470ec8ca70c9..2ccd413fb8984 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<IIncomi <FieldRow> <Box fontScale='p2' withRichContent flexGrow={1}> <pre> - <code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(hilightedExampleJson) }}></code> </pre> </Box> </FieldRow> @@ -179,10 +180,12 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi <FieldHint id={`${channelField}-hint-2`} dangerouslySetInnerHTML={{ - __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { - postProcess: 'sprintf', - sprintf: ['@', '#', '@john', '#general'], - }), + __html: DOMPurify.sanitize( + t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { + postProcess: 'sprintf', + sprintf: ['@', '#', '@john', '#general'], + }), + ), }} /> {errors?.channel && ( @@ -271,7 +274,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi <FieldHint id={`${emojiField}-hint-1`}>{t('You_can_use_an_emoji_as_avatar')}</FieldHint> <FieldHint id={`${emojiField}-hint-2`} - dangerouslySetInnerHTML={{ __html: t('Example_s', { postProcess: 'sprintf', sprintf: [':ghost:'] }) }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Example_s', { postProcess: 'sprintf', sprintf: [':ghost:'] })) }} /> </Field> <Field> diff --git a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx index 3259857f009d9..15db3e86f795f 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 = () => { <FieldHint id={`${channelField}-hint-2`} dangerouslySetInnerHTML={{ - __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { - postProcess: 'sprintf', - sprintf: ['@', '#', '@john', '#general'], - }), + __html: DOMPurify.sanitize( + t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { + postProcess: 'sprintf', + sprintf: ['@', '#', '@john', '#general'], + }), + ), }} /> - <FieldHint id={`${channelField}-hint-3`} dangerouslySetInnerHTML={{ __html: t('Integrations_for_all_channels') }} /> + <FieldHint + id={`${channelField}-hint-3`} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Integrations_for_all_channels')) }} + /> </Field> )} {showTriggerWords && ( @@ -226,10 +232,12 @@ const OutgoingWebhookForm = () => { <FieldHint id={`${targetRoomField}-hint-2`} dangerouslySetInnerHTML={{ - __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { - postProcess: 'sprintf', - sprintf: ['@', '#', '@john', '#general'], - }), + __html: DOMPurify.sanitize( + t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', { + postProcess: 'sprintf', + sprintf: ['@', '#', '@john', '#general'], + }), + ), }} /> </Field> @@ -359,7 +367,7 @@ const OutgoingWebhookForm = () => { <FieldHint id={`${emojiField}-hint-1`}>{t('You_can_use_an_emoji_as_avatar')}</FieldHint> <FieldHint id={`${emojiField}-hint-2`} - dangerouslySetInnerHTML={{ __html: t('Example_s', { postProcess: 'sprintf', sprintf: [':ghost:'] }) }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Example_s', { postProcess: 'sprintf', sprintf: [':ghost:'] })) }} /> </Field> <Field> @@ -435,7 +443,7 @@ const OutgoingWebhookForm = () => { <FieldRow> <Box fontScale='p2' withRichContent flexGrow={1}> <pre> - <code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(hilightedExampleJson) }}></code> </pre> </Box> </FieldRow> @@ -485,7 +493,10 @@ const OutgoingWebhookForm = () => { )} /> </FieldRow> - <FieldHint id={`${retryDelayField}-hint`} dangerouslySetInnerHTML={{ __html: t('Integration_Retry_Delay_Description') }} /> + <FieldHint + id={`${retryDelayField}-hint`} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Integration_Retry_Delay_Description')) }} + /> </Field> {event === 'sendMessage' && ( <FieldGroup> 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 f016ce36fe7c0..d9581600110c8 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<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: dataSentToTriggerCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(dataSentToTriggerCode) }}></code> </pre> </Box> </FieldRow> @@ -122,7 +123,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: prepareSentMessageCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(prepareSentMessageCode) }}></code> </pre> </Box> </FieldRow> @@ -134,7 +135,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: processSentMessageCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(processSentMessageCode) }}></code> </pre> </Box> </FieldRow> @@ -156,7 +157,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: httpCallDataCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(httpCallDataCode) }}></code> </pre> </Box> </FieldRow> @@ -168,7 +169,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: httpErrorCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(httpErrorCode) }}></code> </pre> </Box> </FieldRow> @@ -180,7 +181,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: httpResultCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(httpResultCode) }}></code> </pre> </Box> </FieldRow> @@ -192,7 +193,7 @@ const HistoryItem = ({ data }: { data: Serialized<IIntegrationHistory> }) => { <FieldRow> <Box withRichContent w='full'> <pre> - <code dangerouslySetInnerHTML={{ __html: errorStackCode }}></code> + <code dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(errorStackCode) }}></code> </pre> </Box> </FieldRow> diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index 9f6942368faa7..1a38a80c5d2cc 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} </FieldError> )} - <FieldHint id={`${emailBodyId}-hint`} dangerouslySetInnerHTML={{ __html: t('Mailer_body_tags') }} /> + <FieldHint id={`${emailBodyId}-hint`} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Mailer_body_tags')) }} /> </Field> </FieldGroup> </Box> diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 9fb0a5317b053..484eb34cacfa3 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 && <span dangerouslySetInnerHTML={{ __html: i18n.exists(alert) ? t(alert) : alert }} />, + () => + alert && <span dangerouslySetInnerHTML={{ __html: i18n.exists(alert) ? DOMPurify.sanitize(t(alert)) : DOMPurify.sanitize(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 50f523571a0e5..0bfb6a8101a2c 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={ <span dangerouslySetInnerHTML={{ - __html: t('Custom_oauth_helper', callbackURL(sectionName)), + __html: DOMPurify.sanitize(t('Custom_oauth_helper', callbackURL(sectionName))), }} /> } diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 1ba9cb3ced5aa..fea907c0719c1 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 && ( <FieldHint id={`${verifiedId}-hint`} - dangerouslySetInnerHTML={{ __html: t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' }) }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' })) }} /> )} {!isVerificationNeeded && ( <FieldHint id={`${verifiedId}-hint`} - dangerouslySetInnerHTML={{ __html: t('Email_verification_isnt_required', { url: 'admin/settings/Accounts' }) }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(t('Email_verification_isnt_required', { url: 'admin/settings/Accounts' })), + }} /> )} </> @@ -429,7 +432,7 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD {!isSmtpEnabled && ( <FieldHint id={`${sendWelcomeEmailId}-hint`} - dangerouslySetInnerHTML={{ __html: t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' }) }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' })) }} mbs={0} /> )} diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx index 7b06f59307221..aa47bc24b67fc 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 && ( <FieldHint id={`${setRandomPasswordId}-hint`} - dangerouslySetInnerHTML={{ __html: t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' }) }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Send_Email_SMTP_Warning', { url: 'admin/settings/Email' })) }} mbe={16} mbs={0} /> diff --git a/apps/meteor/client/views/banners/LegacyBanner.tsx b/apps/meteor/client/views/banners/LegacyBanner.tsx index aaf947d195ae1..1227a2a2f8317 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 && <div dangerouslySetInnerHTML={{ __html: typeof html === 'function' ? html() : html }} />} + {html && ( + <div dangerouslySetInnerHTML={{ __html: typeof html === 'function' ? DOMPurify.sanitize(html()) : DOMPurify.sanitize(html) }} /> + )} </Banner> ); }; diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx index 22c54d7b9eda5..e0ae6fad46113 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 = <div dangerouslySetInnerHTML={{ __html: image }} />; + const emojiElement = <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(image) }} />; return ( <IconButton diff --git a/apps/meteor/client/views/composer/EmojiPicker/ToneSelector/ToneItem.tsx b/apps/meteor/client/views/composer/EmojiPicker/ToneSelector/ToneItem.tsx index 65813965ba04d..35251b7ce6461 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/ToneSelector/ToneItem.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/ToneSelector/ToneItem.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; const ToneItem = ({ tone }: { tone: number }) => { let toneEmoji; @@ -23,7 +24,7 @@ const ToneItem = ({ tone }: { tone: number }) => { toneEmoji = '<span class="emojione emojione-people _270b">✋</span>'; } - return <Box dangerouslySetInnerHTML={{ __html: toneEmoji }} />; + return <Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(toneEmoji) }} />; }; export default ToneItem; diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx index 8227cd6da79ef..888afa1d7b3e6 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} > - <Box dangerouslySetInnerHTML={{ __html: t('E2E_password_request_text') }} /> + <Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_password_request_text')) }} /> <FieldGroup mbs={24} w='full'> <Field> <FieldRow> diff --git a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx index c9e7fa1a8abec..1511b71686d5f 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')} > <p> - <span dangerouslySetInnerHTML={{ __html: t('E2E_password_reveal_text', { randomPassword }) }} /> + <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_password_reveal_text', { randomPassword })) }} /> <ExternalLink to={DOCS_URL} mis={4}> {t('Learn_more_about_E2EE')} </ExternalLink> 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 91c18baa01d16..1743fa6013e7e 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 <pre> <code dangerouslySetInnerHTML={{ - __html: useHighlightedCode('json', JSON.stringify(args, null, 2)), + __html: DOMPurify.sanitize(useHighlightedCode('json', JSON.stringify(args, null, 2))), }} /> </pre> 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 1da4c0c4a9789..e409df9ee677c 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 && <FieldHint dangerouslySetInnerHTML={{ __html: t(provider.description as TranslationKey) }} />} + {provider.description && ( + <FieldHint dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t(provider.description as TranslationKey)) }} /> + )} </Field> {globalSearchEnabled && ( <Field> diff --git a/apps/meteor/tests/end-to-end/api/oauthapps.ts b/apps/meteor/tests/end-to-end/api/oauthapps.ts index 5e42069d99341..39fa944223475 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 00b4640295e51..f4509990edc99 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 0462230b97b04..b0cfbf5be4810 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 93ea90a09a3c5..f376a986ae8af 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 <em>{{peer}}</em> peer.", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a8587428ef527..ef0602ab7e753 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 dd6a15a468b0e..332f6c831f55e 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<AllHTMLAttributes<HTMLDivElement>, 'is'>) => { @@ -12,7 +13,7 @@ const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: st return ( <Box {...props} display='flex' alignItems='center'> - <Box className={previewEmojiClass} dangerouslySetInnerHTML={{ __html: emoji }}></Box> + <Box className={previewEmojiClass} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(emoji) }}></Box> <Box mis={4} display='flex' flexDirection='column' maxWidth='x160'> <Box fontScale='c2' withTruncatedText> {name} diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 028983ac29fee..720b698ceda9a 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 67dc916fec2d4..d582e0e7321c8 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 ( <HorizontalWizardLayoutCaption> - <Box withRichContent dangerouslySetInnerHTML={{ __html: loginTerms !== '' ? loginTerms : t('Layout_Login_Terms_Content') }} /> + <Box + withRichContent + dangerouslySetInnerHTML={{ + __html: loginTerms !== '' ? DOMPurify.sanitize(loginTerms) : DOMPurify.sanitize(t('Layout_Login_Terms_Content')), + }} + /> </HorizontalWizardLayoutCaption> ); };