Bladeren bron

fix: sanitizer svg to avoid xss (#16606)

tags/1.1.3
Joel 7 maanden geleden
bovenliggende
commit
16b6ffd915
No account linked to committer's email address
4 gewijzigde bestanden met toevoegingen van 21 en 24 verwijderingen
  1. 9
    17
      web/app/components/base/markdown.tsx
  2. 2
    1
      web/app/components/base/svg-gallery/index.tsx
  3. 3
    2
      web/package.json
  4. 7
    4
      web/pnpm-lock.yaml

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

import { Theme } from '@/types/app' import { Theme } from '@/types/app'
import useTheme from '@/hooks/use-theme' import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames' 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 // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = { const capitalizationLanguageNameMap: Record<string, string> = {
</div> </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 { else {
return ( return (
<SyntaxHighlighter <SyntaxHighlighter
} }
} }


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[] }) { export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) {
const latexContent = flow([ const latexContent = flow([
preprocessThinkTag, preprocessThinkTag,
preprocessLaTeX, preprocessLaTeX,
])(escapeSVGTags(props.content))
])(props.content)


return ( return (
<div className={cn('markdown-body', '!text-text-primary', props.className)}> <div className={cn('markdown-body', '!text-text-primary', props.className)}>

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

import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js' import { SVG } from '@svgdotjs/svg.js'
import ImagePreview from '@/app/components/base/image-uploader/image-preview' import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import DOMPurify from 'dompurify'


export const SVGRenderer = ({ content }: { content: string }) => { export const SVGRenderer = ({ content }: { content: string }) => {
const svgRef = useRef<HTMLDivElement>(null) const svgRef = useRef<HTMLDivElement>(null)


svgRef.current.style.width = `${Math.min(originalWidth, 298)}px` svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`


const rootElement = draw.svg(content)
const rootElement = draw.svg(DOMPurify.sanitize(content))


rootElement.click(() => { rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element)) setImagePreview(svgToDataURL(svgElement as Element))

+ 3
- 2
web/package.json Bestand weergeven

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

+ 7
- 4
web/pnpm-lock.yaml Bestand weergeven

decimal.js: decimal.js:
specifier: ^10.4.3 specifier: ^10.4.3
version: 10.4.3 version: 10.4.3
dompurify:
specifier: ^3.2.4
version: 3.2.4
echarts: echarts:
specifier: ^5.5.1 specifier: ^5.5.1
version: 5.5.1 version: 5.5.1
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'} 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: domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0


dompurify@3.2.3:
dompurify@3.2.4:
optionalDependencies: optionalDependencies:
'@types/trusted-types': 2.0.7 '@types/trusted-types': 2.0.7


d3-sankey: 0.12.3 d3-sankey: 0.12.3
dagre-d3-es: 7.0.11 dagre-d3-es: 7.0.11
dayjs: 1.11.13 dayjs: 1.11.13
dompurify: 3.2.3
dompurify: 3.2.4
katex: 0.16.21 katex: 0.16.21
khroma: 2.1.0 khroma: 2.1.0
lodash-es: 4.17.21 lodash-es: 4.17.21

Laden…
Annuleren
Opslaan