fix(ui): block active HTML in assistant markdown
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { escapeRawMarkdownHtml, MARKDOWN_FORBIDDEN_TAGS } from './markdownSecurity';
|
||||
|
||||
describe('markdown sanitization', () => {
|
||||
test('turns raw assistant HTML into inert visible text', () => {
|
||||
const payload = '<style>@import url("https://example.test/theme.css");</style>';
|
||||
|
||||
expect(escapeRawMarkdownHtml(payload)).toBe(
|
||||
'<style>@import url("https://example.test/theme.css");</style>',
|
||||
);
|
||||
});
|
||||
|
||||
test('forbids script and stylesheet elements as active content', () => {
|
||||
expect(MARKDOWN_FORBIDDEN_TAGS).toContain('script');
|
||||
expect(MARKDOWN_FORBIDDEN_TAGS).toContain('style');
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import DOMPurify from 'dompurify';
|
||||
import { buildAgentMentionUrl, parseAgentHref, parseSkillHref } from '@/lib/messages/inlineMessageLinks';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { highlightCodeInWorker } from './markdown-worker';
|
||||
import { escapeRawMarkdownHtml, MARKDOWN_FORBIDDEN_TAGS } from './markdownSecurity';
|
||||
|
||||
const escapeAttr = (value: string): string =>
|
||||
value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -166,6 +167,12 @@ const parser = marked.use({
|
||||
breaks: false,
|
||||
extensions: [inlineMathExtension, blockMathExtension],
|
||||
renderer: {
|
||||
// Assistant output is untrusted. Markdown constructs still render as HTML,
|
||||
// but raw HTML must remain visible text so it cannot introduce active DOM
|
||||
// such as stylesheets or positioned overlays into the application shell.
|
||||
html({ text }) {
|
||||
return escapeRawMarkdownHtml(text);
|
||||
},
|
||||
link({ href, title, text }) {
|
||||
const target = href ?? '';
|
||||
const agentName = parseAgentHref(target);
|
||||
@@ -283,8 +290,10 @@ const SANITIZE_CONFIG = {
|
||||
USE_PROFILES: { html: true, mathMl: true, svg: true },
|
||||
ADD_TAGS: ['svg', 'path', 'g', 'rect', 'line', 'polygon', 'polyline', 'circle', 'ellipse', 'text', 'tspan', 'defs', 'marker'],
|
||||
ADD_ATTR: ['d', 'viewBox', 'preserveAspectRatio', 'xmlns', 'target', 'fill', 'stroke', 'stroke-width', 'transform', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'style'],
|
||||
FORBID_TAGS: ['script'],
|
||||
FORBID_CONTENTS: ['script'],
|
||||
// Defense in depth for generated/highlighter HTML after raw markdown HTML
|
||||
// has been escaped by the marked renderer above.
|
||||
FORBID_TAGS: [...MARKDOWN_FORBIDDEN_TAGS],
|
||||
FORBID_CONTENTS: [...MARKDOWN_FORBIDDEN_TAGS],
|
||||
};
|
||||
|
||||
let sanitizeHookInstalled = false;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Raw HTML in assistant markdown is untrusted and must stay inert text. */
|
||||
export const escapeRawMarkdownHtml = (value: string): string =>
|
||||
value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
/** Active elements forbidden again at the final DOMPurify boundary. */
|
||||
export const MARKDOWN_FORBIDDEN_TAGS = ['script', 'style'] as const;
|
||||
Reference in New Issue
Block a user