mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
fix(security): sanitize email rich body render path (#5212)
This commit is contained in:
+13
-6
@@ -2378,20 +2378,27 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── WYSIWYG email body helpers ──
|
// ── WYSIWYG email body helpers ──
|
||||||
|
function _emailPlainTextToHtml(text) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.textContent = text == null ? '' : String(text);
|
||||||
|
return d.innerHTML.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
function _emailBodyToHtml(text) {
|
function _emailBodyToHtml(text) {
|
||||||
const t = (text || '').trim();
|
const t = (text || '').trim();
|
||||||
if (!t) return '';
|
if (!t) return '';
|
||||||
// If it already contains a formatting/structural HTML tag, it's a saved
|
// If it already contains a formatting/structural HTML tag, it's a saved
|
||||||
// WYSIWYG body — use it verbatim. (Checking a leading '<' isn't enough: a
|
// WYSIWYG body — sanitize it before rendering. (Checking a leading '<' isn't enough: a
|
||||||
// rich body often starts with plain text, e.g. "Hi <b>there</b>".)
|
// rich body often starts with plain text, e.g. "Hi <b>there</b>".)
|
||||||
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;
|
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 markdownModule.sanitizeAllowedHtml
|
||||||
|
? markdownModule.sanitizeAllowedHtml(t)
|
||||||
|
: _emailPlainTextToHtml(t);
|
||||||
|
}
|
||||||
// Email body: keep author-typed `:shortcode:` text literal. Issue #345
|
// Email body: keep author-typed `:shortcode:` text literal. Issue #345
|
||||||
// (shortcode → emoji) is scoped to chat; do not rewrite colons in mail.
|
// (shortcode → emoji) is scoped to chat; do not rewrite colons in mail.
|
||||||
try { return markdownModule.mdToHtml(text, { shortcodes: false }); }
|
try { return markdownModule.mdToHtml(text, { shortcodes: false }); }
|
||||||
catch (_) {
|
catch (_) { return _emailPlainTextToHtml(text); }
|
||||||
const d = document.createElement('div'); d.textContent = text;
|
|
||||||
return d.innerHTML.replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Mirror the rich body's plain text into the hidden textarea so the existing
|
// Mirror the rich body's plain text into the hidden textarea so the existing
|
||||||
// send / draft / change-detection plumbing (which reads the textarea) stays
|
// send / draft / change-detection plumbing (which reads the textarea) stays
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function _cleanAllowedHtmlOnce(htmlString) {
|
|||||||
return tpl.innerHTML;
|
return tpl.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeAllowedHtml(html) {
|
export function sanitizeAllowedHtml(html) {
|
||||||
const raw = String(html == null ? '' : html);
|
const raw = String(html == null ? '' : html);
|
||||||
// Non-browser context (e.g. a future SSR/Node import): fail closed by
|
// Non-browser context (e.g. a future SSR/Node import): fail closed by
|
||||||
// escaping rather than trusting the markup.
|
// escaping rather than trusting the markup.
|
||||||
@@ -824,6 +824,7 @@ export function renderMermaid(container) {
|
|||||||
const markdownModule = {
|
const markdownModule = {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
|
sanitizeAllowedHtml,
|
||||||
squashOutsideCode,
|
squashOutsideCode,
|
||||||
renderContent,
|
renderContent,
|
||||||
processWithThinking,
|
processWithThinking,
|
||||||
|
|||||||
@@ -23,3 +23,16 @@ def test_markdown_raw_html_sanitizer_strips_scriptable_css():
|
|||||||
assert "if (name === 'style')" in src
|
assert "if (name === 'style')" in src
|
||||||
assert r"javascript:|vbscript:|data:|expression\(" in src
|
assert r"javascript:|vbscript:|data:|expression\(" in src
|
||||||
assert "el.removeAttribute(attr.name);" in src
|
assert "el.removeAttribute(attr.name);" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_email_rich_body_render_path_reuses_raw_html_sanitizer():
|
||||||
|
markdown_src = (_REPO / "static" / "js" / "markdown.js").read_text(encoding="utf-8")
|
||||||
|
document_src = (_REPO / "static" / "js" / "document.js").read_text(encoding="utf-8")
|
||||||
|
email_body_helper = document_src.split("function _emailBodyToHtml(text)", 1)[1].split(
|
||||||
|
" // Mirror the rich body's plain text", 1
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
assert "export function sanitizeAllowedHtml(html)" in markdown_src
|
||||||
|
assert "sanitizeAllowedHtml," in markdown_src
|
||||||
|
assert "markdownModule.sanitizeAllowedHtml(t)" in email_body_helper
|
||||||
|
assert "return t;" not in email_body_helper
|
||||||
|
|||||||
Reference in New Issue
Block a user