Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-points-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)
8 changes: 4 additions & 4 deletions apps/meteor/app/api/server/middlewares/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export function authenticationMiddleware(
if (userId && authToken) {
req.user = (await Users.findOneByIdAndLoginToken(userId as string, hashLoginToken(authToken as string))) || undefined;
} else {
req.user = await oAuth2ServerAuth({
headers: req.headers as Record<string, string | undefined>,
query: req.query as Record<string, string | undefined>,
});
const { authorization } = req.headers;
const accessToken = typeof req.query.access_token === 'string' ? req.query.access_token : undefined;
delete req.query.access_token;
req.user = await oAuth2ServerAuth({ authorization, accessToken });
}

if (config.rejectUnauthorized && !req.user) {
Expand Down
20 changes: 10 additions & 10 deletions apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ async function getAccessToken(accessToken: string) {
return OAuthAccessTokens.findOneByAccessToken(accessToken);
}

export async function oAuth2ServerAuth(partialRequest: {
headers: Record<string, string | undefined>;
query: Record<string, string | undefined>;
}): Promise<IUser | undefined> {
const headerToken = partialRequest.headers.authorization?.replace('Bearer ', '');
const queryToken = partialRequest.query.access_token;
const incomingToken = headerToken || queryToken;
export async function oAuth2ServerAuth(partialRequest: { authorization?: string; accessToken?: string }): Promise<IUser | undefined> {
const headerToken = partialRequest.authorization?.replace('Bearer ', '');
const incomingToken = headerToken || partialRequest.accessToken;

if (!incomingToken) {
return;
Expand Down Expand Up @@ -76,10 +72,14 @@ oauth2server.app.get('/oauth/userinfo', async (req: Request, res: Response) => {
});

API.v1.addAuthMethod((routeContext) => {
const headers = Object.fromEntries(routeContext.request.headers.entries());
const query = (isPlainObject(routeContext.queryParams) ? routeContext.queryParams : {}) as Record<string, string | undefined>;
const authorization = routeContext.request.headers.get('authorization') ?? undefined;
const query = isPlainObject(routeContext.queryParams) ? routeContext.queryParams : {};
const accessToken = typeof query.access_token === 'string' ? query.access_token : undefined;
if (routeContext.queryParams?.access_token) {
delete routeContext.queryParams.access_token;
}

return oAuth2ServerAuth({ headers, query });
return oAuth2ServerAuth({ authorization, accessToken });
});

(WebApp.connectHandlers as unknown as ReturnType<typeof express>).use(oauth2server.app);
34 changes: 11 additions & 23 deletions apps/meteor/client/components/FingerprintChangeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Box } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import DOMPurify from 'dompurify';
import { ExternalLink, GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

import { links } from '../lib/links';

Expand All @@ -24,26 +23,15 @@ const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintCha
confirmText={t('Configuration_update')}
cancelText={t('New_workspace')}
>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('Unique_ID_change_detected_description')),
}}
/>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('Unique_ID_change_detected_learn_more_link', { fingerPrintChangedFaq: links.go.fingerPrintChangedFaq }),
{
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title'],
},
),
}}
/>
<Box is='p' mbe={16}>
<Trans i18nKey='Unique_ID_change_detected_description' />
</Box>
<Box is='p' mbe={16}>
<Trans
i18nKey='Unique_ID_change_detected_learn_more_link'
components={{ a: <ExternalLink to={links.go.fingerPrintChangedFaq} /> }}
/>
</Box>
</GenericModal>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Box } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import DOMPurify from 'dompurify';
import { ExternalLink, GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

import { links } from '../lib/links';

Expand Down Expand Up @@ -30,28 +29,19 @@ const FingerprintChangeModalConfirmation = ({
confirmText={newWorkspace ? t('Confirm_new_workspace') : t('Confirm_configuration_update')}
onClose={onClose}
>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: newWorkspace
? DOMPurify.sanitize(t('Confirm_new_workspace_description'))
: DOMPurify.sanitize(t('Confirm_configuration_update_description')),
}}
/>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('Unique_ID_change_detected_learn_more_link', { fingerPrintChangedFaq: links.go.fingerPrintChangedFaq }),
{
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title'],
},
),
}}
/>
<Box is='p' mbe={16}>
{newWorkspace ? (
<Trans i18nKey='Confirm_new_workspace_description' />
) : (
<Trans i18nKey='Confirm_configuration_update_description' />
)}
</Box>
<Box is='p' mbe={16}>
<Trans
i18nKey='Unique_ID_change_detected_learn_more_link'
components={{ a: <ExternalLink to={links.go.fingerPrintChangedFaq} /> }}
/>
</Box>
</GenericModal>
);
};
Expand Down
10 changes: 3 additions & 7 deletions apps/meteor/client/views/account/security/ChangePassphrase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Box, Field, FieldError, FieldGroup, FieldHint, FieldLabel, FieldRow, Pa
import { PasswordVerifierList } from '@rocket.chat/ui-client';
import { useToastMessageDispatch, usePasswordPolicy } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import DOMPurify from 'dompurify';
import { useEffect, useId } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -99,12 +98,9 @@ export const ChangePassphrase = (): JSX.Element => {

return (
<>
<Box
is='p'
fontScale='p1'
id={e2ePasswordExplanationId}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Encryption_Password_Explanation')) }}
/>
<Box is='p' fontScale='p1' id={e2ePasswordExplanationId}>
<Trans i18nKey='E2E_Encryption_Password_Explanation' />
</Box>
<Box mbs={36} w='full'>
<Box is='h3' fontScale='h4' mbe={12}>
{t('Change_E2EE_password')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import {
} from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import DOMPurify from 'dompurify';
import type { ReactElement, RefObject } from 'react';
import { useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

import AccountTokensRow from './AccountTokensRow';
import AddToken from './AddToken';
Expand Down Expand Up @@ -74,16 +73,9 @@ const AccountTokensTable = (): ReactElement => {

setModal(
<GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={closeModal}>
<Box
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', {
token,
userId,
}),
),
}}
/>
<Box>
<Trans i18nKey='API_Personal_Access_Token_Generated_Text_Token_s_UserId_s' values={{ token, userId }} />
</Box>
</GenericModal>,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, TextInput, Button, Margins, Select, FieldError, FieldGroup, Field, FieldRow } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import { useCallback, useId, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

type AddTokenFormData = {
name: string;
Expand Down Expand Up @@ -53,16 +52,9 @@ const AddToken = ({ reload }: AddTokenProps) => {

setModal(
<GenericModal title={t('API_Personal_Access_Token_Generated')} onConfirm={handleDismissModal} onClose={handleDismissModal}>
<Box
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', {
token,
userId,
}),
),
}}
/>
<Box>
<Trans i18nKey='API_Personal_Access_Token_Generated_Text_Token_s_UserId_s' values={{ token, userId }} />
</Box>
</GenericModal>,
);
} catch (error) {
Expand Down
27 changes: 9 additions & 18 deletions apps/meteor/client/views/admin/integrations/NewBot.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { Box } from '@rocket.chat/fuselage';
import DOMPurify from 'dompurify';
import { useTranslation } from 'react-i18next';
import { ExternalLink } from '@rocket.chat/ui-client';
import { Trans } from 'react-i18next';

const NewBot = () => {
const { t } = useTranslation();

return (
<Box
pb={20}
fontScale='h4'
key='bots'
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('additional_integrations_Bots'), {
ALLOWED_TAGS: ['a'],
ALLOWED_ATTR: ['href', 'target'],
}),
}}
const NewBot = () => (
<Box pb={20} fontScale='h4' key='bots'>
<Trans
i18nKey='additional_integrations_Bots'
components={{ a: <ExternalLink to='https://github.com/RocketChat/hubot-rocketchat' /> }}
/>
);
};
</Box>
);

export default NewBot;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useAbsoluteUrl } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import { useId, useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';

import type { EditIncomingWebhookFormData } from './EditIncomingWebhook';
import useClipboardWithToast from '../../../../hooks/useClipboardWithToast';
Expand Down Expand Up @@ -177,17 +177,9 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi
/>
</FieldRow>
<FieldHint id={`${channelField}-hint-1`}>{t('Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here')}</FieldHint>
<FieldHint
id={`${channelField}-hint-2`}
dangerouslySetInnerHTML={{
__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-2`}>
<Trans i18nKey='Start_with_s_for_user_or_s_for_channel_Eg_s_or_s' components={{ code: <code className='inline' /> }} />
</FieldHint>
{errors?.channel && (
<FieldError aria-live='assertive' id={`${channelField}-error`}>
{errors?.channel.message}
Expand Down Expand Up @@ -272,10 +264,9 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi
/>
</FieldRow>
<FieldHint id={`${emojiField}-hint-1`}>{t('You_can_use_an_emoji_as_avatar')}</FieldHint>
<FieldHint
id={`${emojiField}-hint-2`}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('Example_s', { postProcess: 'sprintf', sprintf: [':ghost:'] })) }}
/>
<FieldHint id={`${emojiField}-hint-2`}>
<Trans i18nKey='Example_s' values={{ value: ':ghost:' }} components={{ code: <code className='inline' /> }} />
</FieldHint>
</Field>
<Field>
<FieldRow>
Expand Down
Loading
Loading