Переглянути джерело

fix: sanitizer svg to avoid xss (#16606)

Joel 1 місяць тому
батько
коміт
16b6ffd915

+ 9 - 17
web/app/components/base/markdown.tsx

@@ -27,6 +27,7 @@ import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
 import { Theme } from '@/types/app'
 import useTheme from '@/hooks/use-theme'
 import cn from '@/utils/classnames'
+import SVGRenderer from './svg-gallery'
 
 // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
 const capitalizationLanguageNameMap: Record<string, string> = {
@@ -136,14 +137,13 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }: any) =>
         </div>
       )
     }
-    // Attention: SVGRenderer has xss vulnerability
-    // else if (language === 'svg' && isSVG) {
-    //   return (
-    //     <ErrorBoundary>
-    //       <SVGRenderer content={content} />
-    //     </ErrorBoundary>
-    //   )
-    // }
+    else if (language === 'svg' && isSVG) {
+      return (
+        <ErrorBoundary>
+          <SVGRenderer content={content} />
+        </ErrorBoundary>
+      )
+    }
     else {
       return (
         <SyntaxHighlighter
@@ -240,19 +240,11 @@ const Link = ({ node, ...props }: any) => {
   }
 }
 
-function escapeSVGTags(htmlString: string): string {
-  return htmlString.replace(/(<svg[\s\S]*?>)([\s\S]*?)(<\/svg>)/gi, (match: string, openTag: string, innerContent: string, closeTag: string): string => {
-    return openTag.replace(/</g, '&lt;').replace(/>/g, '&gt;')
-      + innerContent.replace(/</g, '&lt;').replace(/>/g, '&gt;')
-      + closeTag.replace(/</g, '&lt;').replace(/>/g, '&gt;')
-  })
-}
-
 export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) {
   const latexContent = flow([
     preprocessThinkTag,
     preprocessLaTeX,
-  ])(escapeSVGTags(props.content))
+  ])(props.content)
 
   return (
     <div className={cn('markdown-body', '!text-text-primary', props.className)}>

+ 2 - 1
web/app/components/base/svg-gallery/index.tsx

@@ -1,6 +1,7 @@
 import { useEffect, useRef, useState } from 'react'
 import { SVG } from '@svgdotjs/svg.js'
 import ImagePreview from '@/app/components/base/image-uploader/image-preview'
+import DOMPurify from 'dompurify'
 
 export const SVGRenderer = ({ content }: { content: string }) => {
   const svgRef = useRef<HTMLDivElement>(null)
@@ -44,7 +45,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
 
         svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
 
-        const rootElement = draw.svg(content)
+        const rootElement = draw.svg(DOMPurify.sanitize(content))
 
         rootElement.click(() => {
           setImagePreview(svgToDataURL(svgElement as Element))

+ 3 - 2
web/package.json

@@ -61,6 +61,7 @@
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
     "decimal.js": "^10.4.3",
+    "dompurify": "^3.2.4",
     "echarts": "^5.5.1",
     "echarts-for-react": "^3.0.2",
     "elkjs": "^0.9.3",
@@ -132,10 +133,10 @@
   },
   "devDependencies": {
     "@antfu/eslint-config": "^4.1.1",
-    "@eslint/js": "^9.20.0",
     "@chromatic-com/storybook": "^3.1.0",
     "@eslint-react/eslint-plugin": "^1.15.0",
     "@eslint/eslintrc": "^3.1.0",
+    "@eslint/js": "^9.20.0",
     "@faker-js/faker": "^9.0.3",
     "@next/eslint-plugin-next": "^15.2.3",
     "@rgrove/parse-xml": "^4.1.0",
@@ -174,11 +175,11 @@
     "code-inspector-plugin": "^0.18.1",
     "cross-env": "^7.0.3",
     "eslint": "^9.20.1",
+    "eslint-config-next": "^15.0.0",
     "eslint-plugin-react-hooks": "^5.1.0",
     "eslint-plugin-react-refresh": "^0.4.19",
     "eslint-plugin-storybook": "^0.11.2",
     "eslint-plugin-tailwindcss": "^3.18.0",
-    "eslint-config-next": "^15.0.0",
     "husky": "^9.1.6",
     "jest": "^29.7.0",
     "jest-environment-jsdom": "^29.7.0",

+ 7 - 4
web/pnpm-lock.yaml

@@ -121,6 +121,9 @@ importers:
       decimal.js:
         specifier: ^10.4.3
         version: 10.4.3
+      dompurify:
+        specifier: ^3.2.4
+        version: 3.2.4
       echarts:
         specifier: ^5.5.1
         version: 5.5.1
@@ -4299,8 +4302,8 @@ packages:
     resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
     engines: {node: '>= 4'}
 
-  dompurify@3.2.3:
-    resolution: {integrity: sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==}
+  dompurify@3.2.4:
+    resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
 
   domutils@2.8.0:
     resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -13070,7 +13073,7 @@ snapshots:
     dependencies:
       domelementtype: 2.3.0
 
-  dompurify@3.2.3:
+  dompurify@3.2.4:
     optionalDependencies:
       '@types/trusted-types': 2.0.7
 
@@ -15688,7 +15691,7 @@ snapshots:
       d3-sankey: 0.12.3
       dagre-d3-es: 7.0.11
       dayjs: 1.11.13
-      dompurify: 3.2.3
+      dompurify: 3.2.4
       katex: 0.16.21
       khroma: 2.1.0
       lodash-es: 4.17.21