diff --git a/.changeset/composer-preview-src-validation.md b/.changeset/composer-preview-src-validation.md new file mode 100644 index 0000000000000..c29ab88d781e1 --- /dev/null +++ b/.changeset/composer-preview-src-validation.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Validate `src` URL scheme in the slash command composer preview popup. The `ComposerBoxPopupPreview` component now ignores preview media values that do not resolve to an `http`, `https`, `data`, or `blob` URL, blocking `javascript:` (and other non-media) URIs returned by `/v1/commands.preview`. diff --git a/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx b/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx index dfa02a7829945..691cc13206235 100644 --- a/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx +++ b/apps/meteor/client/views/room/composer/ComposerBoxPopupPreview.tsx @@ -9,6 +9,17 @@ import { useChat } from '../contexts/ChatContext'; type ComposerBoxPopupPreviewItem = { _id: string; type: 'image' | 'video' | 'audio' | 'text' | 'other'; value: string; sort?: number }; +const SAFE_MEDIA_SCHEMES = new Set(['http:', 'https:', 'data:', 'blob:']); + +const safeMediaSrc = (value: string): string | undefined => { + try { + const { protocol } = new URL(value, window.location.origin); + return SAFE_MEDIA_SCHEMES.has(protocol) ? value : undefined; + } catch { + return undefined; + } +}; + type ComposerBoxPopupPreviewProps = ComposerBoxPopupProps & { title?: ReactNode; rid: string; @@ -119,40 +130,44 @@ const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview( .map((_, index) => )} {!isLoading && - itemsFlat.map((item) => ( - select(item)} - role='option' - className={['popup-item', item === focused && 'selected'].filter(Boolean).join(' ')} - id={`popup-item-${item._id}`} - key={item._id} - bg={item === focused ? 'selected' : undefined} - borderColor={item === focused ? 'highlight' : 'transparent'} - tabIndex={item === focused ? 0 : -1} - aria-selected={item === focused} - m={2} - borderWidth='default' - borderRadius='x4' - > - {item.type === 'image' && {item._id}} - {item.type === 'audio' && ( - - )} - {item.type === 'video' && ( - - )} - {item.type === 'text' && } - {item.type === 'other' && {item.value}} - - ))} + itemsFlat.map((item) => { + const mediaSrc = + item.type === 'image' || item.type === 'audio' || item.type === 'video' ? safeMediaSrc(item.value) : undefined; + return ( + select(item)} + role='option' + className={['popup-item', item === focused && 'selected'].filter(Boolean).join(' ')} + id={`popup-item-${item._id}`} + key={item._id} + bg={item === focused ? 'selected' : undefined} + borderColor={item === focused ? 'highlight' : 'transparent'} + tabIndex={item === focused ? 0 : -1} + aria-selected={item === focused} + m={2} + borderWidth='default' + borderRadius='x4' + > + {item.type === 'image' && mediaSrc && {item._id}} + {item.type === 'audio' && mediaSrc && ( + + )} + {item.type === 'video' && mediaSrc && ( + + )} + {item.type === 'text' && } + {item.type === 'other' && {item.value}} + + ); + })}