From 7a830e504df6166307be6c9cb88a3eecf6cb7d65 Mon Sep 17 00:00:00 2001 From: SurprisedDuck Date: Mon, 1 Jun 2026 22:50:53 +0200 Subject: [PATCH] Escape email fold summary metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The email reader folds quoted history into
summaries via `_foldSummary()` (static/js/emailLibrary/signatureFold.js), which builds a sender/date "meta" chip into the summary HTML and assigns it to innerHTML. The server-side thread parser (`_extract_quote_meta`, src/email_thread_parser.py) strips tags but then un-escapes HTML entities and preserves `<...>` patterns, and that raw meta reaches `_foldSummary` unescaped via `_renderTurnsFromServer` (`t.meta`) — so an inbound email whose quoted attribution contains `From: <img src=x onerror=...>` runs script when the victim merely opens the message (stored XSS). Make `_foldSummary` the single escaping chokepoint: escape `primary` and `subMeta` with the module's existing `_esc`. The client-side `_extractQuoteMeta` previously pre-escaped its output, and every consumer of it routes through `_foldSummary`, so drop that now-redundant escaping to avoid double-encoding (e.g. "Ben & Jerry" -> "Ben &amp; Jerry"). Verified (jsdom): server-raw and client-extracted malicious metas yield 0 live elements and 0 event-handler attributes; benign "Ben & Jerry" renders single-escaped. Co-authored-by: Claude --- static/js/emailLibrary/signatureFold.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/static/js/emailLibrary/signatureFold.js b/static/js/emailLibrary/signatureFold.js index 4c3868e..4cd932b 100644 --- a/static/js/emailLibrary/signatureFold.js +++ b/static/js/emailLibrary/signatureFold.js @@ -110,13 +110,18 @@ export function _foldSummary(label, iconSvg, meta) { subMeta = ''; } } + // `meta` is derived from _extractQuoteMeta, which strips tags but then + // un-escapes entities (to recover `` for bubble alignment) — + // so it can carry attacker-controlled angle brackets from a quoted block. + // This summary is built into innerHTML, so escape both parts to stop a + // crafted quote (e.g. `From: `) from running script. const metaSpan = subMeta - ? `` + ? `` : ''; return ( '' @@ -158,9 +163,12 @@ export function _extractQuoteMeta(html) { if (from.length > 60) from = from.slice(0, 57) + '…'; if (date.length > 28) date = date.slice(0, 25) + '…'; - if (from && date) return `${_esc(from)} · ${_esc(date)}`; - if (from) return _esc(from); - if (date) return _esc(date); + // Return the raw sender/date text; `_foldSummary` is the single sink that + // builds these into HTML, so it owns escaping. Escaping here too would + // double-encode (e.g. "Ben & Jerry" -> "Ben &amp; Jerry"). + if (from && date) return `${from} · ${date}`; + if (from) return from; + if (date) return date; return ''; }