Fix: Implement DOMPurify to sanitize HTML content before rendering (#1498)

### What problem does this PR solve?

This PR resolves issue #1491 related to HTML Injection and Cross-Site
Scripting (XSS). The issue was caused by the unsafe usage of
`dangerouslySetInnerHTML` without proper sanitization of user input.

### Changes
- Added DOMPurify dependency.
- Updated the following components to use DOMPurify:
-
`web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx`
  - `web/src/pages/chat/markdown-content/index.tsx`
-
`web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx`

### Type of change

- [x] Other (please describe): Security Fix
This commit is contained in:
Dani Servian 2024-07-15 04:24:23 +02:00 committed by GitHub
parent 2dea8448a6
commit bafe137502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 35 additions and 3 deletions

25
web/package-lock.json generated
View File

@ -17,6 +17,7 @@
"classnames": "^2.5.1",
"dagre": "^0.8.5",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
"elkjs": "^0.9.3",
"eventsource-parser": "^1.1.2",
"human-id": "^4.1.1",
@ -52,6 +53,7 @@
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/dagre": "^0.7.52",
"@types/dompurify": "^3.0.5",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/react": "^18.0.33",
@ -4856,6 +4858,16 @@
"@types/ms": "*"
}
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/eslint": {
"version": "8.56.1",
"resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.56.1.tgz",
@ -5206,6 +5218,13 @@
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.2.tgz",
@ -10715,6 +10734,12 @@
"integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==",
"deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix."
},
"node_modules/dompurify": {
"version": "3.1.6",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.1.6.tgz",
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmmirror.com/domutils/-/domutils-2.8.0.tgz",

View File

@ -28,6 +28,7 @@
"classnames": "^2.5.1",
"dagre": "^0.8.5",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
"elkjs": "^0.9.3",
"eventsource-parser": "^1.1.2",
"human-id": "^4.1.1",
@ -63,6 +64,7 @@
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/dagre": "^0.7.52",
"@types/dompurify": "^3.0.5",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/react": "^18.0.33",

View File

@ -2,6 +2,7 @@ import Image from '@/components/image';
import { IChunk } from '@/interfaces/database/knowledge';
import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd';
import classNames from 'classnames';
import DOMPurify from 'dompurify';
import { useState } from 'react';
import { ChunkTextMode } from '../../constant';
@ -73,7 +74,9 @@ const ChunkCard = ({
className={styles.content}
>
<div
dangerouslySetInnerHTML={{ __html: item.content_with_weight }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.content_with_weight),
}}
className={classNames({
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
})}

View File

@ -2,6 +2,7 @@ import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/commonHooks';
import { useSelectParserList } from '@/hooks/userSettingHook';
import { Col, Divider, Empty, Row, Typography } from 'antd';
import DOMPurify from 'dompurify';
import { useMemo } from 'react';
import styles from './index.less';
import { ImageMap } from './utils';
@ -39,7 +40,7 @@ const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
</Title>
<p
dangerouslySetInnerHTML={{
__html: item.description,
__html: DOMPurify.sanitize(item.description),
}}
></p>
<Title level={5}>

View File

@ -6,6 +6,7 @@ import { IChunk } from '@/interfaces/database/knowledge';
import { getExtension } from '@/utils/documentUtils';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Button, Flex, Popover, Space } from 'antd';
import DOMPurify from 'dompurify';
import { useCallback } from 'react';
import Markdown from 'react-markdown';
import reactStringReplace from 'react-string-replace';
@ -94,7 +95,7 @@ const MarkdownContent = ({
<Space direction={'vertical'}>
<div
dangerouslySetInnerHTML={{
__html: chunkItem?.content_with_weight,
__html: DOMPurify.sanitize(chunkItem?.content_with_weight),
}}
className={styles.chunkContentText}
></div>