diff --git a/client/src/components/Banners/Banner.tsx b/client/src/components/Banners/Banner.tsx index b10fa70941..a1e9056c07 100644 --- a/client/src/components/Banners/Banner.tsx +++ b/client/src/components/Banners/Banner.tsx @@ -1,7 +1,8 @@ -import { useEffect, useRef } from 'react'; +import DOMPurify from 'dompurify'; import { XIcon } from 'lucide-react'; import { useRecoilState } from 'recoil'; import { Button, cn } from '@librechat/client'; +import { useEffect, useMemo, useRef } from 'react'; import { useGetBannerQuery } from '~/data-provider'; import store from '~/store'; @@ -10,6 +11,25 @@ export const Banner = ({ onHeightChange }: { onHeightChange?: (height: number) = const [hideBannerHint, setHideBannerHint] = useRecoilState(store.hideBannerHint); const bannerRef = useRef(null); + const sanitizedMessage = useMemo(() => { + if (!banner?.message) { + return ''; + } + const sanitizer = DOMPurify(); + sanitizer.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName === 'A') { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'noopener noreferrer'); + } + }); + return sanitizer.sanitize(banner.message, { + ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code', 'span'], + ALLOWED_ATTR: ['href', 'class', 'target', 'rel'], + ALLOW_DATA_ATTR: false, + ALLOW_ARIA_ATTR: false, + }); + }, [banner?.message]); + useEffect(() => { if (onHeightChange && bannerRef.current) { onHeightChange(bannerRef.current.offsetHeight); @@ -45,7 +65,7 @@ export const Banner = ({ onHeightChange }: { onHeightChange?: (height: number) = 'text-md w-full truncate text-center [&_a]:text-blue-700 [&_a]:underline dark:[&_a]:text-blue-400', !banner.persistable && 'px-4', )} - dangerouslySetInnerHTML={{ __html: banner.message }} + dangerouslySetInnerHTML={{ __html: sanitizedMessage }} > {!banner.persistable && (