mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-28 11:16:31 +08:00
fix(web): sanitize the parsed markdown tree instead of the string (GHSA-jv5f-3mqh-cxwf) (#17590)
### Summary This PR sanitizes the parsed tree instead of the string: `rehype-sanitize` runs after `rehype-raw` and before `rehype-katex`, with one shared schema.
This commit is contained in:
31
web/package-lock.json
generated
31
web/package-lock.json
generated
@@ -76,6 +76,7 @@
|
||||
"dompurify": "^3.3.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"hast-util-sanitize": "^5.0.2",
|
||||
"human-id": "^4.1.1",
|
||||
"i18next": "^23.7.16",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
@@ -116,6 +117,7 @@
|
||||
"recharts": "^2.12.4",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
@@ -15200,6 +15202,21 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-sanitize": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
|
||||
"integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-select": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/hast-util-select/-/hast-util-select-6.0.4.tgz",
|
||||
@@ -22686,6 +22703,20 @@
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-sanitize": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
|
||||
"integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-sanitize": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-slug": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/rehype-slug/-/rehype-slug-6.0.0.tgz",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"dompurify": "^3.3.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"hast-util-sanitize": "^5.0.2",
|
||||
"human-id": "^4.1.1",
|
||||
"i18next": "^23.7.16",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
@@ -126,6 +127,7 @@
|
||||
"recharts": "^2.12.4",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { RehypeSanitizeAssistantMarkdown } from '@/constants/markdown-rehype-plugins';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
import styles from './floating-chat-widget-markdown.module.less';
|
||||
import { useIsDarkTheme } from './theme-provider';
|
||||
@@ -319,7 +320,12 @@ const FloatingChatWidgetMarkdown = ({
|
||||
return (
|
||||
<div className={styles['floating-chat-widget']} dir={dir}>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw, rehypeWrapReference, rehypeKatex]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
RehypeSanitizeAssistantMarkdown,
|
||||
rehypeWrapReference,
|
||||
rehypeKatex,
|
||||
]}
|
||||
remarkPlugins={MarkdownRemarkPlugins}
|
||||
className="text-sm leading-relaxed space-y-2 prose-sm max-w-full"
|
||||
components={
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { RehypeSanitizeAssistantMarkdown } from '@/constants/markdown-rehype-plugins';
|
||||
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
|
||||
@@ -55,7 +56,11 @@ const HighLightMarkdown = ({
|
||||
<div dir={dir} className={classNames(styles.text)}>
|
||||
<Markdown
|
||||
remarkPlugins={MarkdownRemarkPlugins}
|
||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
RehypeSanitizeAssistantMarkdown,
|
||||
rehypeKatex,
|
||||
]}
|
||||
components={
|
||||
{
|
||||
p: ({ children, ...props }: any) => (
|
||||
|
||||
@@ -27,6 +27,7 @@ import Markdown from 'react-markdown';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { RehypeSanitizeAssistantMarkdown } from '@/constants/markdown-rehype-plugins';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -83,7 +84,12 @@ const rehypeWrapReference = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const MarkdownRehypePlugins = [rehypeRaw, rehypeWrapReference, rehypeKatex];
|
||||
const MarkdownRehypePlugins = [
|
||||
rehypeRaw,
|
||||
RehypeSanitizeAssistantMarkdown,
|
||||
rehypeWrapReference,
|
||||
rehypeKatex,
|
||||
];
|
||||
|
||||
const MarkdownParagraph = ({ children, ...props }: any) => (
|
||||
<p {...props}>{children}</p>
|
||||
|
||||
@@ -28,6 +28,7 @@ import Markdown, { defaultUrlTransform } from 'react-markdown';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { RehypeSanitizeAssistantMarkdown } from '@/constants/markdown-rehype-plugins';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -390,7 +391,12 @@ function MarkdownContent({
|
||||
return (
|
||||
<div dir={dir} className={styles.markdownContentWrapper}>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw, rehypeWrapReference, rehypeKatex]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
RehypeSanitizeAssistantMarkdown,
|
||||
rehypeWrapReference,
|
||||
rehypeKatex,
|
||||
]}
|
||||
remarkPlugins={MarkdownRemarkPlugins}
|
||||
urlTransform={(url, key) => {
|
||||
if (
|
||||
|
||||
50
web/src/constants/markdown-rehype-plugins.ts
Normal file
50
web/src/constants/markdown-rehype-plugins.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defaultSchema } from 'hast-util-sanitize';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import type { PluggableList } from 'unified';
|
||||
|
||||
/**
|
||||
* Allow-list for assistant-rendered markdown.
|
||||
*
|
||||
* The renderers run `rehype-raw`, so raw HTML inside a message becomes real
|
||||
* nodes. Sanitizing the markdown *string* before that cannot hold:
|
||||
* `preprocessLaTeX()` decodes `<`/`>`/`&` back into markup afterwards,
|
||||
* so an entity-encoded payload is inert while the allow-list inspects it and
|
||||
* live again by the time rehype-raw parses it. Sanitizing the tree closes that
|
||||
* gap, and it leaves text nodes alone -- which a string-level pass cannot do,
|
||||
* because escaping `<` breaks `$a < b$` for KaTeX and drops `Array<number>`
|
||||
* inside code fences.
|
||||
*/
|
||||
export const MarkdownSanitizeSchema = {
|
||||
...defaultSchema,
|
||||
tagNames: [
|
||||
...(defaultSchema.tagNames ?? []),
|
||||
// wrappers injected by replaceThinkToSection / replaceRetrievingToSection
|
||||
'think',
|
||||
'retrieving',
|
||||
'section',
|
||||
'details',
|
||||
'summary',
|
||||
],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
'*': [...(defaultSchema.attributes?.['*'] ?? []), 'className'],
|
||||
},
|
||||
protocols: {
|
||||
...defaultSchema.protocols,
|
||||
// Assistant answers legitimately embed inline images as `data:` URLs
|
||||
// (base64 charts, screenshots). `img` is the only src-bearing tag left in
|
||||
// the allow-list -- script/iframe/object/embed are not in `tagNames` -- and
|
||||
// a `data:` payload loaded through <img> cannot execute script, so this
|
||||
// re-enables inline images without re-opening a script sink.
|
||||
src: [...(defaultSchema.protocols?.src ?? []), 'data'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply the allow-list to the parsed tree. Keep this after `rehype-raw` and
|
||||
* before `rehype-katex`: KaTeX emits markup the allow-list does not cover.
|
||||
*/
|
||||
export const RehypeSanitizeAssistantMarkdown: PluggableList[number] = [
|
||||
rehypeSanitize,
|
||||
MarkdownSanitizeSchema,
|
||||
];
|
||||
@@ -25,6 +25,7 @@ import Markdown from 'react-markdown';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { RehypeSanitizeAssistantMarkdown } from '@/constants/markdown-rehype-plugins';
|
||||
import { visitParents } from 'unist-util-visit-parents';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -276,7 +277,12 @@ const MarkdownContent = ({
|
||||
className="[&>section.think]:pl-[10px] [&>section.think]:text-[#8b8b8b] [&>section.think]:border-l-2 [&>section.think]:border-l-[#d5d3d3] [&>section.think]:mb-[10px] [&>section.think]:text-xs [&>blockquote]:pl-[10px] [&>blockquote]:border-l-4 [&>blockquote]:border-l-[#ccc] text-sm"
|
||||
>
|
||||
<Markdown
|
||||
rehypePlugins={[rehypeRaw, rehypeWrapReference, rehypeKatex]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
RehypeSanitizeAssistantMarkdown,
|
||||
rehypeWrapReference,
|
||||
rehypeKatex,
|
||||
]}
|
||||
remarkPlugins={MarkdownRemarkPlugins}
|
||||
components={
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user