fix: sanitize img tags in markdown via shared SafeImg component (#18112)

This commit is contained in:
chanx
2026-08-11 19:08:15 +08:00
committed by GitHub
parent cd6996b301
commit dbe2bf8b8b
4 changed files with 67 additions and 12 deletions

View File

@@ -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) => (
<p {...omit(props, 'node')}>{children}</p>
),
img: SafeImg,
code(props: any) {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');

View File

@@ -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 = ({
<div className={'space-y-2 max-w-[40vw]'}>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(chunkItem?.content ?? ''),
__html: sanitizeHtmlWithImagesAsText(chunkItem?.content ?? ''),
}}
className={classNames(styles.chunkContentText)}
dir="auto"
@@ -303,6 +305,7 @@ const MarkdownContent = ({
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
'custom-typography': ({ children }: { children: string }) =>
renderReference(children),
img: SafeImg,
code(props: any) {
const { children, className, ...rest } = props;
const restProps = omit(rest, 'node');

View File

@@ -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({
</a>
);
},
img({ src, alt, ...props }: any) {
img({ src, alt, title }: any) {
if (isArtifactUrl(src)) {
return (
<ArtifactImage
@@ -429,16 +430,7 @@ function MarkdownContent({
/>
);
}
return (
<span className={styles.artifactImageWrapper}>
<img
src={src}
alt={alt || ''}
className={styles.artifactImage}
{...omit(props, 'node')}
/>
</span>
);
return <SafeImg src={src} alt={alt} title={title} />;
},
code(props: any) {
const { children, className, ...rest } = props;

View File

@@ -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 = '<img';
if (src != null) tag += ` src="${src}"`;
if (alt != null) tag += ` alt="${alt}"`;
if (title != null) tag += ` title="${title}"`;
tag += '>';
return tag;
};
/**
* Drop all attributes except src/alt/title so event handlers (onerror/onload)
* injected via rehypeRaw cannot fire. Used as a react-markdown `img` override.
*/
const isSafeImgSrc = (src: unknown): src is string =>
typeof src === 'string' && SAFE_SRC_REGEXP.test(src.trim());
/**
* Render an <img> safely for react-markdown overrides.
*
* - Safe src (http(s) / relative / `data:image`): render a real <img>,
* 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 `<img>` 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 <img src={src} alt={alt ?? ''} title={title} />;
};
export default SafeImg;