feat(mail) : helper sanitizeMailHtml — DOMPurify + placeholder images distantes

This commit is contained in:
2026-05-20 00:24:30 +02:00
parent e7224765b1
commit bfa155d060

View File

@@ -0,0 +1,160 @@
import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify'
/**
* Options de sanitization du corps HTML d'un mail.
*/
export type SanitizeMailHtmlOptions = {
/**
* Si true, les images distantes (http/https) sont affichées directement.
* Par défaut false — les images distantes sont remplacées par un placeholder
* cliquable pour éviter le tracking par pixel.
*/
allowImages?: boolean
}
/**
* Configuration DOMPurify bloquante pour les corps de mail.
* - Bloque les balises dangereuses : script, iframe, object, embed, style, link, meta, form, input
* - Bloque les attributs événements (on*) et les URI javascript:
* - Autorise les URI data: uniquement pour les images (PNG/JPEG/GIF/WEBP) — images inline CID
*/
const DOMPURIFY_CONFIG: DOMPurifyConfig = {
FORBID_TAGS: [
'script',
'iframe',
'object',
'embed',
'style',
'link',
'meta',
'form',
'input',
'button',
'textarea',
'select',
'base',
'applet',
],
FORBID_ATTR: [
'onerror',
'onload',
'onclick',
'onmouseover',
'onmouseout',
'onmouseenter',
'onmouseleave',
'onfocus',
'onblur',
'onchange',
'onsubmit',
'onreset',
'onkeydown',
'onkeyup',
'onkeypress',
'ondblclick',
'oncontextmenu',
'onwheel',
'ondrag',
'ondrop',
'oncopy',
'oncut',
'onpaste',
'action',
'formaction',
'xlink:href',
],
ALLOWED_URI_REGEXP: /^(?:https?|mailto|tel|cid|data:image\/(?:png|jpeg|gif|webp)(?:;base64,)?)(?::|$)/i,
FORCE_BODY: true,
WHOLE_DOCUMENT: false,
}
/**
* Remplace les balises <img> avec src http(s):// par un bouton placeholder.
* Le src original est stocké en data-mail-image-src pour permettre l'affichage
* à la demande de l'utilisateur (Phase 5 — MailMessageViewer).
*/
function replaceRemoteImages(html: string): string {
// Utiliser un DOMParser côté client uniquement (SSR-safe : le guard process.client
// est géré par l'appelant dans un composant Vue — ce helper ne tourne que client-side)
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const images = doc.querySelectorAll('img')
images.forEach((img) => {
const src = img.getAttribute('src') ?? ''
const isRemote = /^https?:\/\//i.test(src)
if (!isRemote) return
// Remplacer par un span cliquable (pas de <button> — DOMPurify le forbid)
const placeholder = doc.createElement('span')
placeholder.setAttribute('data-mail-image-src', src)
placeholder.setAttribute('data-mail-image-placeholder', 'true')
placeholder.setAttribute('title', src)
placeholder.style.cssText = [
'display: inline-flex',
'align-items: center',
'gap: 4px',
'padding: 2px 6px',
'border: 1px solid #d1d5db',
'border-radius: 4px',
'background: #f9fafb',
'color: #6b7280',
'font-size: 12px',
'cursor: pointer',
'user-select: none',
].join(';')
placeholder.textContent = '[Image distante — cliquer pour afficher]'
img.replaceWith(placeholder)
})
return doc.body.innerHTML
}
/**
* Sanitize le HTML brut d'un corps de mail.
*
* - Bloque tous les vecteurs XSS connus (scripts, événements inline, iframes…)
* - Par défaut, remplace les images distantes par un placeholder anti-tracking
* - Utiliser allowImages: true uniquement si l'utilisateur a explicitement cliqué
* "Afficher les images" dans le lecteur de mail
*
* IMPORTANT : Cette fonction requiert un environnement navigateur (DOMParser, DOMPurify).
* Ne pas appeler côté SSR — toujours dans un composant Vue avec `onMounted` ou dans
* un computed côté client uniquement (`import.meta.client`).
*
* @param rawHtml - HTML brut tel que reçu de l'API backend
* @param options - Options de sanitization
* @returns HTML sanitizé, sûr pour injection via v-html
*/
export function sanitizeMailHtml(
rawHtml: string,
options: SanitizeMailHtmlOptions = {},
): string {
if (!rawHtml || rawHtml.trim() === '') return ''
// Étape 1 : DOMPurify — supprime tous les vecteurs dangereux
const sanitized = DOMPurify.sanitize(rawHtml, DOMPURIFY_CONFIG) as string
// Étape 2 : Remplacement images distantes (anti-tracking)
if (!options.allowImages) {
return replaceRemoteImages(sanitized)
}
return sanitized
}
/**
* Vérifie si un élément HTML est un placeholder d'image généré par sanitizeMailHtml.
* Utile dans MailMessageViewer pour gérer le clic "Afficher l'image".
*/
export function isMailImagePlaceholder(el: HTMLElement): boolean {
return el.hasAttribute('data-mail-image-placeholder')
}
/**
* Récupère le src original d'un placeholder d'image.
*/
export function getMailImageSrc(el: HTMLElement): string | null {
return el.getAttribute('data-mail-image-src')
}