mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-12 20:03:40 +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 { 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 || '');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
58
web/src/components/safe-img.tsx
Normal file
58
web/src/components/safe-img.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user