mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-09-08 10:14:35 +08:00
fix: sanitize img tags in markdown via shared SafeImg component (#18112)
This commit is contained in:
@@ -32,6 +32,7 @@ import { citationMarkerReg } from '@/utils/citation-utils';
|
|||||||
import { getDirAttribute } from '@/utils/text-direction';
|
import { getDirAttribute } from '@/utils/text-direction';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { useIsDarkTheme } from '../theme-provider';
|
import { useIsDarkTheme } from '../theme-provider';
|
||||||
|
import { SafeImg } from '@/components/safe-img';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
const HighLightMarkdown = ({
|
const HighLightMarkdown = ({
|
||||||
@@ -60,6 +61,7 @@ const HighLightMarkdown = ({
|
|||||||
p: ({ children, ...props }: any) => (
|
p: ({ children, ...props }: any) => (
|
||||||
<p {...omit(props, 'node')}>{children}</p>
|
<p {...omit(props, 'node')}>{children}</p>
|
||||||
),
|
),
|
||||||
|
img: SafeImg,
|
||||||
code(props: any) {
|
code(props: any) {
|
||||||
const { children, className, ...rest } = props;
|
const { children, className, ...rest } = props;
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '../ui/hover-card';
|
} from '../ui/hover-card';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
import { sanitizeHtmlWithImagesAsText } from '@/utils/dom-util';
|
||||||
|
import { SafeImg } from '@/components/safe-img';
|
||||||
|
|
||||||
const getChunkIndex = (match: string) => parseCitationIndex(match);
|
const getChunkIndex = (match: string) => parseCitationIndex(match);
|
||||||
|
|
||||||
@@ -211,7 +213,7 @@ const MarkdownContent = ({
|
|||||||
<div className={'space-y-2 max-w-[40vw]'}>
|
<div className={'space-y-2 max-w-[40vw]'}>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: DOMPurify.sanitize(chunkItem?.content ?? ''),
|
__html: sanitizeHtmlWithImagesAsText(chunkItem?.content ?? ''),
|
||||||
}}
|
}}
|
||||||
className={classNames(styles.chunkContentText)}
|
className={classNames(styles.chunkContentText)}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
@@ -303,6 +305,7 @@ const MarkdownContent = ({
|
|||||||
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
||||||
'custom-typography': ({ children }: { children: string }) =>
|
'custom-typography': ({ children }: { children: string }) =>
|
||||||
renderReference(children),
|
renderReference(children),
|
||||||
|
img: SafeImg,
|
||||||
code(props: any) {
|
code(props: any) {
|
||||||
const { children, className, ...rest } = props;
|
const { children, className, ...rest } = props;
|
||||||
const restProps = omit(rest, 'node');
|
const restProps = omit(rest, 'node');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import Image, { AuthenticatedImg } from '@/components/image';
|
import Image, { AuthenticatedImg } from '@/components/image';
|
||||||
import SvgIcon from '@/components/svg-icon';
|
import SvgIcon from '@/components/svg-icon';
|
||||||
|
import { SafeImg } from '@/components/safe-img';
|
||||||
import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins';
|
import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins';
|
||||||
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
|
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
|
||||||
import { getExtension } from '@/utils/document-util';
|
import { getExtension } from '@/utils/document-util';
|
||||||
@@ -419,7 +420,7 @@ function MarkdownContent({
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
img({ src, alt, ...props }: any) {
|
img({ src, alt, title }: any) {
|
||||||
if (isArtifactUrl(src)) {
|
if (isArtifactUrl(src)) {
|
||||||
return (
|
return (
|
||||||
<ArtifactImage
|
<ArtifactImage
|
||||||
@@ -429,16 +430,7 @@ function MarkdownContent({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <SafeImg src={src} alt={alt} title={title} />;
|
||||||
<span className={styles.artifactImageWrapper}>
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
alt={alt || ''}
|
|
||||||
className={styles.artifactImage}
|
|
||||||
{...omit(props, 'node')}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
code(props: any) {
|
code(props: any) {
|
||||||
const { children, className, ...rest } = props;
|
const { children, className, ...rest } = props;
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user