Skip to main content

max / goingson

Harden email-body linkifier against href attribute breakout Consolidate the bracketed and bare-URL linkifiers in formatEmailBody behind one anchor builder that runs the href through safeUrl + escapeAttrValue, and tighten the bracketed capture to [^\]\s"]+. Closes the CONFIRMED CRITICAL remote-email -> arbitrary-Tauri-command chain (escapeHtml does not encode ", so the old [^\]]+ capture let a plain-text email break out of the href and attach an event handler). Sibling of the Run #28 bare-URL hardening; the shared builder prevents the two matchers from drifting apart again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-07 14:08 UTC
Commit: 6e9e6ca5fbe021b591cba515b0131fbd33f1d598
Parent: 4644fd6
1 file changed, +14 insertions, -12 deletions
@@ -295,19 +295,21 @@ function formatEmailBody(body) {
295 295 // Escape any remaining HTML for XSS protection
296 296 let escaped = escapeHtml(text);
297 297
298 - // Convert [url] patterns to clickable links
298 + // Convert [url] patterns to clickable links, then bare URLs. Both routes
299 + // share one anchor builder so their char classes and escaping can't drift
300 + // apart again (the bracketed matcher previously used `[^\]]+`, which let `"`
301 + // and spaces through and broke out of the href attribute — a sibling of the
302 + // Run #28 bare-URL hardening that this consolidation closes for good).
303 + // `url` is already escapeHtml'd (safe as link text); the href is run through
304 + // safeUrl (scheme allow-list) then escapeAttrValue (encodes `"`/`&`).
305 + const linkAnchor = (_match, url) =>
306 + `<a href="${escapeAttrValue(safeUrl(url))}" class="email-link" target="_blank" rel="noopener noreferrer">${url}</a>`;
307 +
299 308 // Pattern: text [https://...] or text [http://...]
300 - escaped = escaped.replace(
301 - /\[((https?:\/\/)[^\]]+)\]/g,
302 - '<a href="$1" class="email-link" target="_blank" rel="noopener noreferrer">$1</a>'
303 - );
304 -
305 - // Also detect bare URLs that weren't wrapped
306 - // Match URLs that aren't already inside an href or anchor tag
307 - escaped = escaped.replace(
308 - /(?<!href="|">)(https?:\/\/[^\s<>\[\]"]+)/g,
309 - '<a href="$1" class="email-link" target="_blank" rel="noopener noreferrer">$1</a>'
310 - );
309 + escaped = escaped.replace(/\[((https?:\/\/)[^\]\s"]+)\]/g, linkAnchor);
310 +
311 + // Also detect bare URLs that weren't wrapped (not already inside an href/anchor)
312 + escaped = escaped.replace(/(?<!href="|">)(https?:\/\/[^\s<>\[\]"]+)/g, linkAnchor);
311 313
312 314 // Collapse quoted blocks (> lines and "On ... wrote:" attribution)
313 315 const lines = escaped.split('\n');