From 9e64d9a0268b0d972741cdfcff951f00fa215c45 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:15:04 +0200 Subject: [PATCH] fix: drop remote images from plugin-authored HTML so a plugin cannot beacon past its allowed origins --- lib/__tests__/email-sanitization.test.ts | 11 +++++++++++ lib/email-sanitization.ts | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/email-sanitization.test.ts b/lib/__tests__/email-sanitization.test.ts index b08e3dee1..7ba25b9d5 100644 --- a/lib/__tests__/email-sanitization.test.ts +++ b/lib/__tests__/email-sanitization.test.ts @@ -832,6 +832,17 @@ describe('email-sanitization', () => { expect(sanitizePluginBodyHtml('')).toBe(''); expect(sanitizePluginBodyHtml(' ')).toBe(''); }); + + it('drops remote images, which would carry data to any origin from the host page', () => { + // The sandbox CSP keeps a plugin to its manifest's httpOrigins; an + // https: image in text the host renders is a request to anywhere. + const out = sanitizePluginBodyHtml( + '

x

', + ); + expect(out).not.toContain('attacker.example'); + expect(out).toContain('data:image/png;base64,'); + expect(out).toContain('cid:part1'); + }); }); // ALLOWED_URI_REGEXP excludes image/svg+xml on purpose (DOMPurify cannot see diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts index 278cacf16..3600ef6c3 100644 --- a/lib/email-sanitization.ts +++ b/lib/email-sanitization.ts @@ -262,9 +262,25 @@ const PLUGIN_BODY_SANITIZE_CONFIG = { ALLOWED_TAGS: [...SIGNATURE_SANITIZE_CONFIG.ALLOWED_TAGS, 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'pre', 'code', 's', 'hr'], }; +/** + * Drop images in plugin-authored HTML unless they are embedded. A plugin's + * network reach is its manifest's `httpOrigins`, enforced by the sandbox + * CSP; an `https:` image the host page then loads for it is a request to + * any origin, carrying whatever the plugin put in the URL. A user's own + * signature may keep remote images (restrictSignatureImages); a plugin's + * text may not. + */ +function restrictPluginImages(node: Element): void { + if (node.tagName !== 'IMG') return; + const src = node.getAttribute('src'); + if (!src || !/^(?:cid:|data:image\/(?:png|jpe?g|gif|webp);base64,)/i.test(src)) { + node.remove(); + } +} + export function sanitizePluginBodyHtml(html: string): string { if (!html?.trim()) return ''; - DOMPurify.addHook('afterSanitizeAttributes', restrictSignatureImages); + DOMPurify.addHook('afterSanitizeAttributes', restrictPluginImages); try { return DOMPurify.sanitize(html, PLUGIN_BODY_SANITIZE_CONFIG); } finally {