From 3e626e774740adfa41d073337c4920913106ae78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Thu, 10 Sep 2026 15:47:05 +0200 Subject: [PATCH] fix(markdown): sanitize attribute values and prevent Vue template compilation for rendered markdown. --- client/src/components/markdown.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/src/components/markdown.ts b/client/src/components/markdown.ts index 38c6c357..c5f56f6c 100644 --- a/client/src/components/markdown.ts +++ b/client/src/components/markdown.ts @@ -58,7 +58,9 @@ function htmlTag( let attributeString = '' for (const attr in attributes) { if (Object.prototype.hasOwnProperty.call(attributes, attr) && attributes[attr]) { - attributeString += ` ${attr}="${attributes[attr]}"` // md.sanitizeText(attr) + // attribute values must be escaped: they are attacker-controlled and are + // rendered as raw HTML (see render() below), never as a compiled template + attributeString += ` ${attr}="${md.sanitizeText(attributes[attr])}"` } } @@ -208,13 +210,15 @@ const rules: MarkdownRules = { } }, html(node, output, state) { + // rendered as inert HTML (see render() below), so use a plain title + // attribute for the tooltip instead of a Vue directive expression return htmlTag( 'span', '', { class: `emoji`, 'data-emoji': node.id, - 'v-tooltip.top-center': `{ content:':${node.id}:', offset: 2, delay: { show: 1000, hide: 100 } }`, + title: `:${node.id}:`, }, state, ) @@ -264,6 +268,13 @@ export default class extends Vue { escapeHTML: true, cssModuleNames: null, } - return h({ template: `
${htmlOutput(parser(this.source, state), state)}
` }) + + // IMPORTANT: never compile the rendered markdown as a Vue template (e.g. via + // `h({ template: ... })`). Doing so would let attacker-controlled chat + // messages be parsed as Vue template syntax (e.g. `{{ constructor... }}`), + // resulting in arbitrary JavaScript execution in other users' browsers. + // Setting innerHTML directly renders the sanitized markup as inert HTML + // without ever running it through the Vue compiler. + return h('div', { domProps: { innerHTML: htmlOutput(parser(this.source, state), state) } }) } }