mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
fix: mermaid graph (#29811)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
isMermaidCodeComplete,
|
||||
prepareMermaidCode,
|
||||
processSvgForTheme,
|
||||
sanitizeMermaidCode,
|
||||
svgToBase64,
|
||||
waitForDOMElement,
|
||||
} from './utils'
|
||||
@@ -71,7 +72,7 @@ const initMermaid = () => {
|
||||
const config: MermaidConfig = {
|
||||
startOnLoad: false,
|
||||
fontFamily: 'sans-serif',
|
||||
securityLevel: 'loose',
|
||||
securityLevel: 'strict',
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
@@ -267,6 +268,8 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
finalCode = prepareMermaidCode(primitiveCode, look)
|
||||
}
|
||||
|
||||
finalCode = sanitizeMermaidCode(finalCode)
|
||||
|
||||
// Step 2: Render chart
|
||||
const svgGraph = await renderMermaidChart(finalCode, look)
|
||||
|
||||
@@ -297,9 +300,9 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
const configureMermaid = useCallback((primitiveCode: string) => {
|
||||
if (typeof window !== 'undefined' && isInitialized) {
|
||||
const themeVars = THEMES[currentTheme]
|
||||
const config: any = {
|
||||
const config: MermaidConfig = {
|
||||
startOnLoad: false,
|
||||
securityLevel: 'loose',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: 'sans-serif',
|
||||
maxTextSize: 50000,
|
||||
gantt: {
|
||||
@@ -325,7 +328,8 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
config.theme = currentTheme === 'dark' ? 'dark' : 'neutral'
|
||||
|
||||
if (isFlowchart) {
|
||||
config.flowchart = {
|
||||
type FlowchartConfigWithRanker = NonNullable<MermaidConfig['flowchart']> & { ranker?: string }
|
||||
const flowchartConfig: FlowchartConfigWithRanker = {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
nodeSpacing: 60,
|
||||
@@ -333,6 +337,7 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
curve: 'linear',
|
||||
ranker: 'tight-tree',
|
||||
}
|
||||
config.flowchart = flowchartConfig as unknown as MermaidConfig['flowchart']
|
||||
}
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
@@ -531,7 +536,7 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
|
||||
{isLoading && !svgString && (
|
||||
<div className='px-[26px] py-4'>
|
||||
<LoadingAnim type='text'/>
|
||||
<LoadingAnim type='text' />
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('common.wait_for_completion', 'Waiting for diagram code to complete...')}
|
||||
</div>
|
||||
@@ -564,7 +569,7 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
{errMsg && (
|
||||
<div className={themeClasses.errorMessage}>
|
||||
<div className="flex items-center">
|
||||
<ExclamationTriangleIcon className={themeClasses.errorIcon}/>
|
||||
<ExclamationTriangleIcon className={themeClasses.errorIcon} />
|
||||
<span className="ml-2">{errMsg}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cleanUpSvgCode } from './utils'
|
||||
import { cleanUpSvgCode, prepareMermaidCode, sanitizeMermaidCode } from './utils'
|
||||
|
||||
describe('cleanUpSvgCode', () => {
|
||||
it('replaces old-style <br> tags with the new style', () => {
|
||||
@@ -6,3 +6,54 @@ describe('cleanUpSvgCode', () => {
|
||||
expect(result).toEqual('<br/>test<br/>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeMermaidCode', () => {
|
||||
it('removes click directives to prevent link/callback injection', () => {
|
||||
const unsafeProtocol = ['java', 'script:'].join('')
|
||||
const input = [
|
||||
'gantt',
|
||||
'title Demo',
|
||||
'section S1',
|
||||
'Task 1 :a1, 2020-01-01, 1d',
|
||||
`click A href "${unsafeProtocol}alert(location.href)"`,
|
||||
'click B call callback()',
|
||||
].join('\n')
|
||||
|
||||
const result = sanitizeMermaidCode(input)
|
||||
|
||||
expect(result).toContain('gantt')
|
||||
expect(result).toContain('Task 1')
|
||||
expect(result).not.toContain('click A')
|
||||
expect(result).not.toContain('click B')
|
||||
expect(result).not.toContain(unsafeProtocol)
|
||||
})
|
||||
|
||||
it('removes Mermaid init directives to prevent config overrides', () => {
|
||||
const input = [
|
||||
'%%{init: {"securityLevel":"loose"}}%%',
|
||||
'graph TD',
|
||||
'A-->B',
|
||||
].join('\n')
|
||||
|
||||
const result = sanitizeMermaidCode(input)
|
||||
|
||||
expect(result).toEqual(['graph TD', 'A-->B'].join('\n'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareMermaidCode', () => {
|
||||
it('sanitizes click directives in flowcharts', () => {
|
||||
const unsafeProtocol = ['java', 'script:'].join('')
|
||||
const input = [
|
||||
'graph TD',
|
||||
'A[Click]-->B',
|
||||
`click A href "${unsafeProtocol}alert(1)"`,
|
||||
].join('\n')
|
||||
|
||||
const result = prepareMermaidCode(input, 'classic')
|
||||
|
||||
expect(result).toContain('graph TD')
|
||||
expect(result).not.toContain('click ')
|
||||
expect(result).not.toContain(unsafeProtocol)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,28 @@ export function cleanUpSvgCode(svgCode: string): string {
|
||||
return svgCode.replaceAll('<br>', '<br/>')
|
||||
}
|
||||
|
||||
export const sanitizeMermaidCode = (mermaidCode: string): string => {
|
||||
if (!mermaidCode || typeof mermaidCode !== 'string')
|
||||
return ''
|
||||
|
||||
return mermaidCode
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
const trimmed = line.trimStart()
|
||||
|
||||
// Mermaid directives can override config; treat as untrusted in chat context.
|
||||
if (trimmed.startsWith('%%{'))
|
||||
return false
|
||||
|
||||
// Mermaid click directives can create JS callbacks/links inside rendered SVG.
|
||||
if (trimmed.startsWith('click '))
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares mermaid code for rendering by sanitizing common syntax issues.
|
||||
* @param {string} mermaidCode - The mermaid code to prepare
|
||||
@@ -12,10 +34,7 @@ export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'hand
|
||||
if (!mermaidCode || typeof mermaidCode !== 'string')
|
||||
return ''
|
||||
|
||||
let code = mermaidCode.trim()
|
||||
|
||||
// Security: Sanitize against javascript: protocol in click events (XSS vector)
|
||||
code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2')
|
||||
let code = sanitizeMermaidCode(mermaidCode.trim())
|
||||
|
||||
// Convenience: Basic BR replacement. This is a common and safe operation.
|
||||
code = code.replace(/<br\s*\/?>/g, '\n')
|
||||
|
||||
Reference in New Issue
Block a user