fix(desktop): block markdown style injection (#904)

Forbid style tags and inline style attributes in rendered assistant markdown so model text blocks cannot inject global CSS into the desktop UI.

Tested: cd desktop && bun run test -- src/components/markdown/MarkdownRenderer.test.tsx --run
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Not-tested: bun run check:desktop still fails on unrelated existing MessageList timestamp and TabBar deleteSession test issues.
Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-02 21:00:32 +08:00
parent a169cba613
commit 1d20434746
2 changed files with 46 additions and 4 deletions

View File

@ -263,6 +263,45 @@ describe('MarkdownRenderer', () => {
expect(link).toHaveAttribute('rel', expect.stringContaining('noopener'))
})
it('strips style tags from assistant text before injecting markdown html', () => {
const { container } = render(
<MarkdownRenderer
content={[
'开始构建完整的 Web Linux 桌面环境。',
'<function=Write>',
'<parameter=content>',
'<!DOCTYPE html>',
'<html lang="zh-CN">',
'<head>',
'<style>',
'* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }',
'html, body { width: 100%; height: 100%; overflow: hidden; }',
'</style>',
'</head>',
'<body>unsafe preview content</body>',
'</html>',
].join('\n')}
/>,
)
expect(container.querySelector('style')).not.toBeInTheDocument()
expect(container).not.toHaveTextContent('overflow: hidden')
expect(screen.getByText(/开始构建完整的 Web Linux 桌面环境/)).toBeInTheDocument()
})
it('strips inline style attributes from assistant markdown html', () => {
const { container } = render(
<MarkdownRenderer
content={'<div style="position: fixed; inset: 0; z-index: 999999">assistant text</div>'}
/>,
)
const injectedDiv = screen.getByText('assistant text')
expect(injectedDiv).toBeInTheDocument()
expect(injectedDiv).not.toHaveAttribute('style')
expect(container.innerHTML).not.toContain('position: fixed')
})
it('lets callers intercept markdown link clicks', () => {
const onLinkClick = vi.fn().mockReturnValue(true)
render(

View File

@ -39,6 +39,12 @@ const MERMAID_DIAGRAM_START = /^(graph|flowchart|sequenceDiagram|classDiagram|st
const CODE_FENCE_START = /^ {0,3}(`{3,}|~{3,})/
const MATH_RENDER_CACHE_LIMIT = 200
const mathRenderCache = new Map<string, string>()
const MARKDOWN_SANITIZE_CONFIG = {
ADD_TAGS: ['use'],
ADD_ATTR: ['xlink:href'],
FORBID_TAGS: ['style'],
FORBID_ATTR: ['style'],
}
function normalizeCodeLanguage(language: string | undefined): string | undefined {
const normalized = language?.trim().split(/\s+/)[0]?.toLowerCase()
@ -267,10 +273,7 @@ function renderMath(block: MathBlock): string {
}
function enhanceMarkdownHtml(html: string, mathBlocks: MathBlock[]): string {
const cleanHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['use'],
ADD_ATTR: ['xlink:href'],
})
const cleanHtml = DOMPurify.sanitize(html, MARKDOWN_SANITIZE_CONFIG)
const needsDomEnhancement = mathBlocks.length > 0 || /<(?:a|table)\b/i.test(cleanHtml)
if (!needsDomEnhancement) {