From 5951998aad756673747b3e5d6c97909ec6124880 Mon Sep 17 00:00:00 2001 From: dreamhunter2333 Date: Fri, 3 Jul 2026 23:53:15 +0800 Subject: [PATCH] fix: block additional external image sources --- .../src/components/MailContentRenderer.vue | 4 ++ frontend/src/utils/mail-html.js | 48 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/MailContentRenderer.vue b/frontend/src/components/MailContentRenderer.vue index 8ec4d787..1dd18558 100644 --- a/frontend/src/components/MailContentRenderer.vue +++ b/frontend/src/components/MailContentRenderer.vue @@ -180,6 +180,10 @@ const handleSaveToS3 = async (filename, blob) => { style="height: 100vh;">
+ + {{ t('loadExternalImages') }} +
{{ mail.text }}
diff --git a/frontend/src/utils/mail-html.js b/frontend/src/utils/mail-html.js index 4a3de69c..cb4c00e6 100644 --- a/frontend/src/utils/mail-html.js +++ b/frontend/src/utils/mail-html.js @@ -18,6 +18,31 @@ const srcsetHasExternalUrl = (value) => { return value.split(',').some((item) => isExternalImageUrl(item.trim().split(/\s+/)[0])); }; +const blockSourceSet = (element) => { + const srcset = element.getAttribute('srcset') || ''; + if (!srcsetHasExternalUrl(srcset)) return; + + element.setAttribute('data-blocked-srcset', srcset); + element.setAttribute('srcset', `${EXTERNAL_IMAGE_PLACEHOLDER} 1x`); +}; + +const blockStyleExternalImages = (element) => { + const style = element.getAttribute('style') || ''; + if (!/url\(\s*(['"]?)(https?:|\/\/)/i.test(style)) return; + + element.setAttribute('data-blocked-style', style); + element.removeAttribute('style'); +}; + +const blockExternalHref = (element) => { + const href = element.getAttribute('href') || element.getAttribute('xlink:href') || ''; + if (!isExternalImageUrl(href)) return; + + element.setAttribute('data-blocked-href', href); + element.removeAttribute('href'); + element.removeAttribute('xlink:href'); +}; + export const applyExternalImagePolicy = (html, autoLoadExternalImages) => { if (autoLoadExternalImages || !html) { return html || ''; @@ -30,17 +55,32 @@ export const applyExternalImagePolicy = (html, autoLoadExternalImages) => { const hasDocumentShell = /]/i.test(html); const doc = new DOMParser().parseFromString(html, 'text/html'); + for (const base of doc.querySelectorAll('base')) { + base.remove(); + } + + for (const source of doc.querySelectorAll('source[srcset]')) { + blockSourceSet(source); + } + + for (const element of doc.querySelectorAll('[style]')) { + blockStyleExternalImages(element); + } + + for (const element of doc.querySelectorAll('image[href], image[xlink\\:href], use[href], use[xlink\\:href]')) { + blockExternalHref(element); + } + for (const image of doc.querySelectorAll('img')) { const src = image.getAttribute('src') || ''; - const srcset = image.getAttribute('srcset') || ''; - if (!isExternalImageUrl(src) && !srcsetHasExternalUrl(srcset)) { + blockSourceSet(image); + + if (!isExternalImageUrl(src) && !image.getAttribute('data-blocked-srcset')) { continue; } if (src) image.setAttribute('data-blocked-src', src); - if (srcset) image.setAttribute('data-blocked-srcset', srcset); - image.removeAttribute('srcset'); image.setAttribute('src', EXTERNAL_IMAGE_PLACEHOLDER); image.setAttribute('loading', 'lazy'); image.style.maxWidth = '100%';