diff --git a/.changeset/chubby-garlics-decide.md b/.changeset/chubby-garlics-decide.md new file mode 100644 index 0000000000000..4350c7a318cf4 --- /dev/null +++ b/.changeset/chubby-garlics-decide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps': patch +--- + +Replaces {} with Object.create(null) to ensure defense-in-depth against prototype pollution diff --git a/.changeset/neat-planets-hope.md b/.changeset/neat-planets-hope.md new file mode 100644 index 0000000000000..250758fd51940 --- /dev/null +++ b/.changeset/neat-planets-hope.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds custom-sounds.delete API endpoint. diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts index 02e97c7f151be..4a33b3eb21bc1 100644 --- a/apps/meteor/app/api/server/v1/custom-sounds.ts +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -5,17 +5,20 @@ import { isCustomSoundsGetOneProps, isCustomSoundsListProps, isCustomSoundsCreateProps, + isCustomSoundsDeleteProps, isCustomSoundsUpdateProps, ajv, validateBadRequestErrorResponse, validateNotFoundErrorResponse, validateForbiddenErrorResponse, validateUnauthorizedErrorResponse, + validateInternalErrorResponse, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { MAX_CUSTOM_SOUND_SIZE_BYTES, CUSTOM_SOUND_ALLOWED_MIME_TYPES } from '../../../../lib/constants'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { deleteCustomSound } from '../../../custom-sounds/server/lib/deleteCustomSound'; import { insertOrUpdateSound } from '../../../custom-sounds/server/lib/insertOrUpdateSound'; import { uploadCustomSound } from '../../../custom-sounds/server/lib/uploadCustomSound'; import { getExtension, getMimeTypeFromFileName } from '../../../utils/lib/mimeTypes'; @@ -58,6 +61,18 @@ const updateCustomSoundsResponse = ajv.compile<{ success: boolean }>({ required: ['success'], }); +const deleteCustomSoundsResponse = ajv.compile({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], +}); + const customSoundsEndpoints = API.v1 .get( 'custom-sounds.list', @@ -280,6 +295,39 @@ const customSoundsEndpoints = API.v1 return API.v1.failure(error instanceof Error ? error.message : 'Unknown error'); } }, + ) + .post( + 'custom-sounds.delete', + { + response: { + 200: deleteCustomSoundsResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, + 500: validateInternalErrorResponse, + }, + authRequired: true, + body: isCustomSoundsDeleteProps, + permissionsRequired: ['manage-sounds'], + }, + async function action() { + const { _id } = this.bodyParams; + + try { + await deleteCustomSound(_id); + + return API.v1.success(); + } catch (error: unknown) { + this.logger.error({ error }); + + if (error instanceof Meteor.Error && error.error === 'Custom_Sound_Error_Invalid_Sound') { + return API.v1.failure(error.error); + } + + return API.v1.internalError(); + } + }, ); export type CustomSoundEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts b/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts new file mode 100644 index 0000000000000..ec5721164b1fe --- /dev/null +++ b/apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts @@ -0,0 +1,20 @@ +import { api } from '@rocket.chat/core-services'; +import { CustomSounds } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; + +export const deleteCustomSound = async (_id: string): Promise => { + const sound = await CustomSounds.findOneById(_id); + + if (!sound) { + throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', { + method: 'deleteCustomSound', + }); + } + + await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`); + await CustomSounds.removeById(_id); + + void api.broadcast('notify.deleteCustomSound', { soundData: sound }); +}; diff --git a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts index 1c63c7a67d9fc..5b122fa5e116f 100644 --- a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts +++ b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts @@ -1,11 +1,11 @@ -import { api } from '@rocket.chat/core-services'; import type { ICustomSound } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { CustomSounds } from '@rocket.chat/models'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { deleteCustomSound } from '../lib/deleteCustomSound'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,24 +16,12 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async deleteCustomSound(_id) { - let sound = null; - - if (this.userId && (await hasPermissionAsync(this.userId, 'manage-sounds'))) { - sound = await CustomSounds.findOneById(_id); - } else { + methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete'); + if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { throw new Meteor.Error('not_authorized'); } - - if (sound == null) { - throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', { - method: 'deleteCustomSound', - }); - } - - await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`); - await CustomSounds.removeById(_id); - void api.broadcast('notify.deleteCustomSound', { soundData: sound }); - + check(_id, String); + await deleteCustomSound(_id); return true; }, }); diff --git a/apps/meteor/client/components/Emoji.tsx b/apps/meteor/client/components/Emoji.tsx index 46e36459537c6..accd2517b20e1 100644 --- a/apps/meteor/client/components/Emoji.tsx +++ b/apps/meteor/client/components/Emoji.tsx @@ -1,5 +1,4 @@ import styled from '@rocket.chat/styled'; -import type { ReactElement } from 'react'; import { getEmojiClassNameAndDataTitle } from '../lib/utils/renderEmoji'; @@ -20,7 +19,7 @@ const EmojiComponent = styled('span', ({ fillContainer: _fillContainer, ...props : ''} `; -function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps): ReactElement { +function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps) { const { className: emojiClassName, image, ...props } = getEmojiClassNameAndDataTitle(emojiHandle); return ( diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index 066d77334cf44..170451eba0233 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,6 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; import { ExternalLink, GenericModal } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { links } from '../lib/links'; @@ -11,7 +10,7 @@ type FingerprintChangeModalProps = { onClose: () => void; }; -const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { +const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps) => { const { t } = useTranslation(); return ( { +const FingerprintChangeModalConfirmation = ({ onConfirm, onCancel, onClose, newWorkspace }: FingerprintChangeModalConfirmationProps) => { const { t } = useTranslation(); return ( , 'variant' | 'children' | 'onClose' | 'onDismiss'> & { - subtitle?: string | ReactElement; - description?: string | ReactElement; + subtitle?: ReactNode; + description?: ReactNode; img: ComponentProps['src']; - imgWidth?: ComponentProps['width']; imgHeight?: ComponentProps['height']; imgAlt?: string; diff --git a/apps/meteor/client/components/ListSkeleton.tsx b/apps/meteor/client/components/ListSkeleton.tsx index 034f167bca0e8..f507ccdd42b0b 100644 --- a/apps/meteor/client/components/ListSkeleton.tsx +++ b/apps/meteor/client/components/ListSkeleton.tsx @@ -1,5 +1,4 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; import { memo, useMemo } from 'react'; const availablePercentualWidths = [47, 68, 75, 82]; @@ -8,7 +7,7 @@ type ListSkeletonProps = { listCount?: number; }; -const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps): ReactElement => { +const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps) => { const widths = useMemo( () => Array.from({ length: listCount }, (_, index) => `${availablePercentualWidths[index % availablePercentualWidths.length]}%`), [listCount], diff --git a/apps/meteor/client/components/LocalTime.tsx b/apps/meteor/client/components/LocalTime.tsx index be5133050e22b..ded8e707ed4cd 100644 --- a/apps/meteor/client/components/LocalTime.tsx +++ b/apps/meteor/client/components/LocalTime.tsx @@ -1,4 +1,3 @@ -import type { ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +7,7 @@ type LocalTimeProps = { utcOffset: number; }; -const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => { +const LocalTime = ({ utcOffset }: LocalTimeProps) => { const time = useUTCClock(utcOffset); const { t } = useTranslation(); diff --git a/apps/meteor/client/components/NotFoundState.tsx b/apps/meteor/client/components/NotFoundState.tsx index 47911f496b7ec..6fd3fe363807b 100644 --- a/apps/meteor/client/components/NotFoundState.tsx +++ b/apps/meteor/client/components/NotFoundState.tsx @@ -1,6 +1,5 @@ import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useRouter } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; type NotFoundProps = { @@ -8,7 +7,7 @@ type NotFoundProps = { subtitle: string; }; -const NotFoundState = ({ title, subtitle }: NotFoundProps): ReactElement => { +const NotFoundState = ({ title, subtitle }: NotFoundProps) => { const { t } = useTranslation(); const router = useRouter(); diff --git a/apps/meteor/client/components/PageSkeleton.tsx b/apps/meteor/client/components/PageSkeleton.tsx index ca6b78d3c0257..19f7b682fad55 100644 --- a/apps/meteor/client/components/PageSkeleton.tsx +++ b/apps/meteor/client/components/PageSkeleton.tsx @@ -1,8 +1,7 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; -const PageSkeleton = (): ReactElement => ( +const PageSkeleton = () => ( }> diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index 58ae89e4d041c..975c2100761df 100644 --- a/apps/meteor/client/components/RawText.tsx +++ b/apps/meteor/client/components/RawText.tsx @@ -1,9 +1,6 @@ import DOMPurify from 'dompurify'; -import type { ReactElement } from 'react'; /** @deprecated */ -const RawText = ({ children }: { children: string }): ReactElement => ( - -); +const RawText = ({ children }: { children: string }) => ; export default RawText; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 5ebc5393b0847..e4e9472464670 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -6,7 +6,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; +import type { ReactNode } from 'react'; import { forwardRef, memo, useMemo, useState } from 'react'; type LabelType = { name: string; avatarETag?: string; type: IRoom['t']; encrypted?: IRoom['encrypted'] }; @@ -19,7 +19,7 @@ const generateQuery = ( type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular'; - renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null; + renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactNode; setSelectedRoom?: (room: IRoom | undefined) => void; }; diff --git a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx index 401a8879e6093..99d558b72f9d5 100644 --- a/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx +++ b/apps/meteor/client/components/RoomAutoCompleteMultiple/RoomAutoCompleteMultiple.tsx @@ -3,7 +3,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ReactElement, ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; import { memo, useMemo, useState } from 'react'; const generateQuery = ( @@ -16,7 +16,7 @@ type RoomAutoCompleteProps = Omit, 'filter'> readOnly?: boolean; }; -const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps): ReactElement => { +const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps) => { const [filter, setFilter] = useState(''); const filterDebounced = useDebouncedValue(filter, 300); const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate'); @@ -50,7 +50,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet filter={filter} setFilter={setFilter} multiple - renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => ( + renderSelected={({ selected: { value, label }, onRemove, ...props }) => ( @@ -58,7 +58,7 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet )} - renderItem={({ value, label, ...props }): ReactElement => ( + renderItem={({ value, label, ...props }) => (