From dbe2bf8b8ba9d26d99c894eeb99e808f87e412fa Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Tue, 11 Aug 2026 19:08:15 +0800 Subject: [PATCH] fix: sanitize img tags in markdown via shared SafeImg component (#18112) --- .../components/highlight-markdown/index.tsx | 2 + web/src/components/markdown-content/index.tsx | 5 +- .../next-markdown-content/index.tsx | 14 +---- web/src/components/safe-img.tsx | 58 +++++++++++++++++++ 4 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 web/src/components/safe-img.tsx diff --git a/web/src/components/highlight-markdown/index.tsx b/web/src/components/highlight-markdown/index.tsx index 71c3a26885..0dd38f4f83 100644 --- a/web/src/components/highlight-markdown/index.tsx +++ b/web/src/components/highlight-markdown/index.tsx @@ -32,6 +32,7 @@ import { citationMarkerReg } from '@/utils/citation-utils'; import { getDirAttribute } from '@/utils/text-direction'; import { omit } from 'lodash'; import { useIsDarkTheme } from '../theme-provider'; +import { SafeImg } from '@/components/safe-img'; import styles from './index.module.less'; const HighLightMarkdown = ({ @@ -60,6 +61,7 @@ const HighLightMarkdown = ({ p: ({ children, ...props }: any) => (

{children}

), + img: SafeImg, code(props: any) { const { children, className, ...rest } = props; const match = /language-(\w+)/.exec(className || ''); diff --git a/web/src/components/markdown-content/index.tsx b/web/src/components/markdown-content/index.tsx index 744f05120e..b86c04b80d 100644 --- a/web/src/components/markdown-content/index.tsx +++ b/web/src/components/markdown-content/index.tsx @@ -57,6 +57,8 @@ import { HoverCardTrigger, } from '../ui/hover-card'; import styles from './index.module.less'; +import { sanitizeHtmlWithImagesAsText } from '@/utils/dom-util'; +import { SafeImg } from '@/components/safe-img'; const getChunkIndex = (match: string) => parseCitationIndex(match); @@ -211,7 +213,7 @@ const MarkdownContent = ({

{children}

, 'custom-typography': ({ children }: { children: string }) => renderReference(children), + img: SafeImg, code(props: any) { const { children, className, ...rest } = props; const restProps = omit(rest, 'node'); diff --git a/web/src/components/next-markdown-content/index.tsx b/web/src/components/next-markdown-content/index.tsx index 834c81b230..b2cea608c2 100644 --- a/web/src/components/next-markdown-content/index.tsx +++ b/web/src/components/next-markdown-content/index.tsx @@ -16,6 +16,7 @@ import Image, { AuthenticatedImg } from '@/components/image'; import SvgIcon from '@/components/svg-icon'; +import { SafeImg } from '@/components/safe-img'; import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins'; import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat'; import { getExtension } from '@/utils/document-util'; @@ -419,7 +420,7 @@ function MarkdownContent({ ); }, - img({ src, alt, ...props }: any) { + img({ src, alt, title }: any) { if (isArtifactUrl(src)) { return ( ); } - return ( - - {alt - - ); + return ; }, code(props: any) { const { children, className, ...rest } = props; diff --git a/web/src/components/safe-img.tsx b/web/src/components/safe-img.tsx new file mode 100644 index 0000000000..fbfc791459 --- /dev/null +++ b/web/src/components/safe-img.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2026 The InfiniFlow Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ReactNode } from 'react'; + +interface SafeImgProps { + src?: unknown; + alt?: string; + title?: string; +} + +const SAFE_SRC_REGEXP = /^(https?:|\/|\.\/|\.\.\/|data:image\/)/i; + +const buildImgTagString = (src?: unknown, alt?: string, title?: string) => { + let tag = ' + typeof src === 'string' && SAFE_SRC_REGEXP.test(src.trim()); + +/** + * Render an safely for react-markdown overrides. + * + * - Safe src (http(s) / relative / `data:image`): render a real , + * explicitly picking only src/alt/title so event handlers like `onerror` / + * `onload` that rehypeRaw may pass through cannot execute. + * - Non-string or unsafe src (`javascript:` / `vbscript:` / unknown + * schemes): render the original `` tag as a literal string. React + * escapes it, so the markup stays visible without executing scripts. + */ +export const SafeImg = ({ src, alt, title }: SafeImgProps): ReactNode => { + if (!isSafeImgSrc(src)) return <>{buildImgTagString(src, alt, title)}; + return {alt; +}; + +export default SafeImg;