Compare commits

...

1 Commits

Author SHA1 Message Date
arvinxx 5fb46399d2 fix RCE 2026-01-04 10:23:46 +08:00
4 changed files with 171 additions and 3 deletions
+119 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { sanitizeSVGContent } from './sanitize';
import { sanitizeMermaidContent, sanitizeSVGContent } from './sanitize';
describe('sanitizeSVGContent', () => {
it('should preserve safe SVG elements and attributes', () => {
@@ -106,3 +106,121 @@ describe('sanitizeSVGContent', () => {
</g></svg>`);
});
});
describe('sanitizeMermaidContent', () => {
it('should preserve safe Mermaid diagram content', () => {
const safeMermaid = `
graph TD;
A[Start] --> B{Decision}
B -->|Yes| C[OK]
B -->|No| D[End]
`;
const sanitized = sanitizeMermaidContent(safeMermaid);
expect(sanitized).toContain('graph TD;');
expect(sanitized).toContain('A[Start]');
expect(sanitized).toContain('B{Decision}');
});
it('should remove XSS attack via img onerror in HTML label', () => {
const maliciousMermaid = `
graph TD;
A["<img src=x onerror='alert(1)'>"];
`;
const sanitized = sanitizeMermaidContent(maliciousMermaid);
expect(sanitized).not.toContain('onerror');
expect(sanitized).not.toContain('alert');
expect(sanitized).not.toContain('<img');
expect(sanitized).toContain('graph TD;');
});
it('should remove script tags from HTML labels', () => {
const maliciousMermaid = `
graph TD;
A["<script>malicious()</script>"];
B["Normal Label"];
`;
const sanitized = sanitizeMermaidContent(maliciousMermaid);
expect(sanitized).not.toContain('<script>');
expect(sanitized).not.toContain('malicious');
expect(sanitized).toContain('A[""]');
expect(sanitized).toContain('B["Normal Label"]');
});
it('should remove event handlers from HTML labels', () => {
const maliciousMermaid = `
graph TD;
A["<div onclick='evil()'>Click me</div>"];
B["<span onmouseover='hack()'>Hover</span>"];
`;
const sanitized = sanitizeMermaidContent(maliciousMermaid);
expect(sanitized).not.toContain('onclick');
expect(sanitized).not.toContain('onmouseover');
expect(sanitized).not.toContain('evil');
expect(sanitized).not.toContain('hack');
});
it('should handle the PoC RCE attack via electronAPI', () => {
// This is the actual PoC from the vulnerability report
const maliciousMermaid = `
graph TD;
A["<img src=x onerror='window.electronAPI.invoke(String.fromCharCode(114,117,110,67,111,109,109,97,110,100),{command:String.fromCharCode(99,97,108,99,46,101,120,101)})'>"];
`;
const sanitized = sanitizeMermaidContent(maliciousMermaid);
expect(sanitized).not.toContain('onerror');
expect(sanitized).not.toContain('electronAPI');
expect(sanitized).not.toContain('String.fromCharCode');
expect(sanitized).not.toContain('<img');
});
it('should preserve safe formatting tags in labels', () => {
const safeMermaid = `
graph TD;
A["<b>Bold</b> and <i>italic</i>"];
B["<strong>Strong</strong>"];
`;
const sanitized = sanitizeMermaidContent(safeMermaid);
expect(sanitized).toContain('<b>Bold</b>');
expect(sanitized).toContain('<i>italic</i>');
expect(sanitized).toContain('<strong>Strong</strong>');
});
it('should handle nested brackets correctly', () => {
const mermaid = `
graph TD;
A[["Subroutine"]];
B[("Database")];
`;
const sanitized = sanitizeMermaidContent(mermaid);
expect(sanitized).toContain('A[["Subroutine"]]');
expect(sanitized).toContain('B[("Database")]');
});
it('should handle empty content gracefully', () => {
expect(sanitizeMermaidContent('')).toBe('');
});
it('should handle content without HTML labels', () => {
const simpleMermaid = `
graph LR
A --> B --> C
`;
const sanitized = sanitizeMermaidContent(simpleMermaid);
expect(sanitized).toBe(simpleMermaid);
});
});
+33
View File
@@ -31,3 +31,36 @@ export const sanitizeSVGContent = (content: string): string => {
USE_PROFILES: { svg: true, svgFilters: true },
});
};
/**
* Sanitizes Mermaid diagram content to prevent XSS attacks.
*
* Mermaid supports HTML labels in certain node syntaxes like `A["<html>"]`.
* This function strips potentially dangerous HTML content from these labels
* while preserving the diagram structure.
*
* @param content - The Mermaid diagram content to sanitize
* @returns Sanitized Mermaid content safe for rendering
*/
export const sanitizeMermaidContent = (content: string): string => {
// Match HTML labels in Mermaid: ["..."], [("...")], etc.
// These patterns allow HTML rendering in Mermaid when securityLevel is 'loose'
// We need to sanitize any HTML-like content within them
// Pattern to match Mermaid HTML label syntaxes:
// - ["..."] - Standard HTML label
// - [("...")] - Stadium-shaped with HTML
// - [["..."]] - Subroutine shape with HTML
const htmlLabelPattern = /(\[+\(?["'])([\s\S]*?)(["']\)?\]+)/g;
return content.replaceAll(htmlLabelPattern, (match, prefix, labelContent, suffix) => {
// Sanitize the label content to remove dangerous HTML
const sanitized = DOMPurify.sanitize(labelContent, {
ALLOWED_TAGS: ['b', 'i', 'u', 'br', 'em', 'strong', 'sub', 'sup', 'small'],
ALLOWED_ATTR: [],
KEEP_CONTENT: true,
});
return `${prefix}${sanitized}${suffix}`;
});
};
@@ -0,0 +1,16 @@
import { sanitizeMermaidContent } from '@lobechat/utils/client';
import { Mermaid } from '@lobehub/ui';
import { useMemo } from 'react';
interface MermaidRendererProps {
content: string;
}
const MermaidRenderer = ({ content }: MermaidRendererProps) => {
// Sanitize Mermaid content to prevent XSS attacks
const sanitizedContent = useMemo(() => sanitizeMermaidContent(content), [content]);
return <Mermaid variant={'borderless'}>{sanitizedContent}</Mermaid>;
};
export default MermaidRenderer;
@@ -1,8 +1,9 @@
import { Markdown, Mermaid } from '@lobehub/ui';
import { Markdown } from '@lobehub/ui';
import dynamic from 'next/dynamic';
import { memo } from 'react';
import HTMLRenderer from './HTML';
import MermaidRenderer from './Mermaid';
import SVGRender from './SVG';
const ReactRenderer = dynamic(() => import('./React'), { ssr: false });
@@ -18,7 +19,7 @@ const Renderer = memo<{ content: string; type?: string }>(({ content, type }) =>
}
case 'application/lobe.artifacts.mermaid': {
return <Mermaid variant={'borderless'}>{content}</Mermaid>;
return <MermaidRenderer content={content} />;
}
case 'text/markdown': {