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 {