Prevent self-XSS when chat bubbles render user/LLM markdown via dangerouslySetInnerHTML by escaping raw HTML after math/code extraction and sanitizing link href schemes. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { renderInlineMarkdown } from './renderMarkdown';
|
|
import { sanitizeMermaidCode } from '../components/doc/blocks/MermaidBlockView';
|
|
|
|
test('renderInlineMarkdown: renders bold, italic, strikethrough', () => {
|
|
const input = '**粗体指标** *斜体说明* ~~废弃版本~~';
|
|
const html = renderInlineMarkdown(input);
|
|
|
|
assert.ok(html.includes('<strong>粗体指标</strong>'));
|
|
assert.ok(html.includes('<em>斜体说明</em>'));
|
|
assert.ok(html.includes('<del style="color:var(--text-muted)">废弃版本</del>'));
|
|
});
|
|
|
|
test('renderInlineMarkdown: escapes raw HTML tags to prevent XSS', () => {
|
|
const scriptHtml = renderInlineMarkdown('<script>alert(1)</script>');
|
|
assert.ok(!scriptHtml.includes('<script'));
|
|
assert.ok(scriptHtml.includes('<script>'));
|
|
|
|
const imgHtml = renderInlineMarkdown('<img src=x onerror=alert(1)>');
|
|
assert.ok(!/<img[\s>]/i.test(imgHtml), 'must not emit executable img tag');
|
|
assert.ok(imgHtml.includes('<img'));
|
|
|
|
const underlineHtml = renderInlineMarkdown('<u>重要条款</u>');
|
|
assert.ok(!underlineHtml.includes('<u>'));
|
|
assert.ok(underlineHtml.includes('<u>'));
|
|
});
|
|
|
|
test('renderInlineMarkdown: blocks javascript: link hrefs', () => {
|
|
const html = renderInlineMarkdown('[click me](javascript:alert(1))');
|
|
assert.ok(!html.includes('javascript:'));
|
|
assert.ok(html.includes('href="#"'));
|
|
});
|
|
|
|
test('renderInlineMarkdown: renders inline code and preserved line breaks', () => {
|
|
const input = '配置 `MIPI-CSI2` 接口\n第二行内容';
|
|
const html = renderInlineMarkdown(input, { preserveNewlines: true });
|
|
|
|
assert.ok(html.includes('<code'));
|
|
assert.ok(html.includes('MIPI-CSI2'));
|
|
assert.ok(html.includes('<br/>'));
|
|
});
|
|
|
|
test('renderInlineMarkdown: renders headings lists and tables for chat replies', () => {
|
|
const input = `# 标题一
|
|
## 小节
|
|
|
|
- 项目 A
|
|
- 项目 B
|
|
|
|
| 参数 | 值 |
|
|
| --- | --- |
|
|
| 频率 | 24kHz |
|
|
|
|
---
|
|
|
|
**结论**:可行。`;
|
|
const html = renderInlineMarkdown(input, { preserveNewlines: true });
|
|
|
|
assert.ok(html.includes('<h1'));
|
|
assert.ok(html.includes('标题一'));
|
|
assert.ok(html.includes('<h2'));
|
|
assert.ok(html.includes('<ul'));
|
|
assert.ok(html.includes('<table'));
|
|
assert.ok(html.includes('24kHz'));
|
|
assert.ok(html.includes('<hr'));
|
|
assert.ok(html.includes('<strong>结论</strong>'));
|
|
});
|
|
|
|
test('sanitizeMermaidCode: auto wraps unquoted labels containing brackets or comparison operators', () => {
|
|
const rawMermaid = `graph LR
|
|
LaserSource[NIR近红外与白光LED复用光源] --> FPGA[FPGA双路色彩配准与超低延迟融合引擎]
|
|
FPGA --> Display[4K医用级超低延迟监视器 (延时 < 35ms)]
|
|
Display --> CleanNode["已用引号包裹的 (正常节点)"]`;
|
|
|
|
const sanitized = sanitizeMermaidCode(rawMermaid);
|
|
|
|
// 原有含括号未包裹的节点必须被加上双引号
|
|
assert.ok(sanitized.includes('Display["4K医用级超低延迟监视器 (延时 < 35ms)"]'));
|
|
// 已有双引号的节点保持不变
|
|
assert.ok(sanitized.includes('CleanNode["已用引号包裹的 (正常节点)"]'));
|
|
});
|