Skip to content

Commit d8d98ca

Browse files
fix(security): sanitize email rich body render path (#5212)
1 parent 440d99d commit d8d98ca

3 files changed

Lines changed: 28 additions & 7 deletions

File tree

static/js/document.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,20 +2378,27 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
23782378
}
23792379

23802380
// ── WYSIWYG email body helpers ──
2381+
function _emailPlainTextToHtml(text) {
2382+
const d = document.createElement('div');
2383+
d.textContent = text == null ? '' : String(text);
2384+
return d.innerHTML.replace(/\n/g, '<br>');
2385+
}
2386+
23812387
function _emailBodyToHtml(text) {
23822388
const t = (text || '').trim();
23832389
if (!t) return '';
23842390
// If it already contains a formatting/structural HTML tag, it's a saved
2385-
// WYSIWYG body — use it verbatim. (Checking a leading '<' isn't enough: a
2391+
// WYSIWYG body — sanitize it before rendering. (Checking a leading '<' isn't enough: a
23862392
// rich body often starts with plain text, e.g. "Hi <b>there</b>".)
2387-
if (/<\/?(b|i|u|s|strong|em|del|strike|a|p|div|br|ul|ol|li|h[1-3]|blockquote|span|code|pre)\b[^>]*>/i.test(t)) return t;
2393+
if (/<\/?(b|i|u|s|strong|em|del|strike|a|p|div|br|ul|ol|li|h[1-3]|blockquote|span|code|pre)\b[^>]*>/i.test(t)) {
2394+
return markdownModule.sanitizeAllowedHtml
2395+
? markdownModule.sanitizeAllowedHtml(t)
2396+
: _emailPlainTextToHtml(t);
2397+
}
23882398
// Email body: keep author-typed `:shortcode:` text literal. Issue #345
23892399
// (shortcode → emoji) is scoped to chat; do not rewrite colons in mail.
23902400
try { return markdownModule.mdToHtml(text, { shortcodes: false }); }
2391-
catch (_) {
2392-
const d = document.createElement('div'); d.textContent = text;
2393-
return d.innerHTML.replace(/\n/g, '<br>');
2394-
}
2401+
catch (_) { return _emailPlainTextToHtml(text); }
23952402
}
23962403
// Mirror the rich body's plain text into the hidden textarea so the existing
23972404
// send / draft / change-detection plumbing (which reads the textarea) stays

static/js/markdown.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function _cleanAllowedHtmlOnce(htmlString) {
133133
return tpl.innerHTML;
134134
}
135135

136-
function sanitizeAllowedHtml(html) {
136+
export function sanitizeAllowedHtml(html) {
137137
const raw = String(html == null ? '' : html);
138138
// Non-browser context (e.g. a future SSR/Node import): fail closed by
139139
// escaping rather than trusting the markup.
@@ -824,6 +824,7 @@ export function renderMermaid(container) {
824824
const markdownModule = {
825825
escapeHtml,
826826
mdToHtml,
827+
sanitizeAllowedHtml,
827828
squashOutsideCode,
828829
renderContent,
829830
processWithThinking,

tests/test_markdown_dom_xss_helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,16 @@ def test_markdown_raw_html_sanitizer_strips_scriptable_css():
2323
assert "if (name === 'style')" in src
2424
assert r"javascript:|vbscript:|data:|expression\(" in src
2525
assert "el.removeAttribute(attr.name);" in src
26+
27+
28+
def test_email_rich_body_render_path_reuses_raw_html_sanitizer():
29+
markdown_src = (_REPO / "static" / "js" / "markdown.js").read_text(encoding="utf-8")
30+
document_src = (_REPO / "static" / "js" / "document.js").read_text(encoding="utf-8")
31+
email_body_helper = document_src.split("function _emailBodyToHtml(text)", 1)[1].split(
32+
" // Mirror the rich body's plain text", 1
33+
)[0]
34+
35+
assert "export function sanitizeAllowedHtml(html)" in markdown_src
36+
assert "sanitizeAllowedHtml," in markdown_src
37+
assert "markdownModule.sanitizeAllowedHtml(t)" in email_body_helper
38+
assert "return t;" not in email_body_helper

0 commit comments

Comments
 (0)