From 2ddc2bf00e898b661409b797519bc03421137b4c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:20:21 +0200 Subject: [PATCH] fix: keep lists, quotes and headings in the HTML plugins put into a message --- lib/__tests__/plugin-host-composer.test.ts | 10 ++++++++++ lib/email-sanitization.ts | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/__tests__/plugin-host-composer.test.ts b/lib/__tests__/plugin-host-composer.test.ts index 38196c56a..dec0bbc79 100644 --- a/lib/__tests__/plugin-host-composer.test.ts +++ b/lib/__tests__/plugin-host-composer.test.ts @@ -66,6 +66,16 @@ describe('api.composer', () => { expect(handle.calls[1][1]).toContain('Hi'); }); + it('keeps lists, quotes and headings a plugin sends as HTML', async () => { + const handle = fakeHandle(); + const { id } = registerComposer(handle); + await call(WRITE, 'composer.replaceSelection', id, { html: '

Files

q
' }); + const html = handle.calls[0][1] as string; + expect(html).toContain(''); + expect(html).toContain('

Files

'); + expect(html).toContain('
q
'); + }); + it('reaches only a composer that is open, by the id it handed out', async () => { const { unregister } = registerComposer(fakeHandle()); await expect(call(READ, 'composer.getBody', 'guess')).rejects.toThrow(/not open any more/); diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts index 95eb2c5c4..278cacf16 100644 --- a/lib/email-sanitization.ts +++ b/lib/email-sanitization.ts @@ -252,11 +252,24 @@ export function sanitizeSignatureHtmlForDisplay(html: string): string { * * An untrusted plugin runs in a null-origin iframe, but whatever it returns * here lands in the host document and in the message the user then sends, so - * it gets the same allowlist as a user-authored signature: formatting, links, + * it gets the allowlist of a user-authored signature: formatting, links, * images and tables, no script, no event handlers, no external URI schemes. + * Plus the structure the composer itself can write (lists, quotes, headings, + * code), so a plugin's list of links or rewritten paragraph keeps its shape. */ +const PLUGIN_BODY_SANITIZE_CONFIG = { + ...SIGNATURE_SANITIZE_CONFIG, + ALLOWED_TAGS: [...SIGNATURE_SANITIZE_CONFIG.ALLOWED_TAGS, 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'pre', 'code', 's', 'hr'], +}; + export function sanitizePluginBodyHtml(html: string): string { - return sanitizeSignatureHtml(html); + if (!html?.trim()) return ''; + DOMPurify.addHook('afterSanitizeAttributes', restrictSignatureImages); + try { + return DOMPurify.sanitize(html, PLUGIN_BODY_SANITIZE_CONFIG); + } finally { + DOMPurify.removeAllHooks(); + } } /**