From 977b9e817e59ce58a61030696d016fcaf5a86d6d Mon Sep 17 00:00:00 2001 From: Guillaume DENIS Date: Wed, 8 Jul 2026 15:25:24 +0200 Subject: [PATCH] =?UTF-8?q?fix(email-template):=20s=C3=A9curise=20et=20fia?= =?UTF-8?q?bilise=20le=20rendu=20des=20mails=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - emailUrl(): whitelist de scheme (http/https/mailto/tel, data:image) et échappement des guillemets sur les URLs de liens et d'images — bloque l'injection d'attribut et les schemes javascript:/data:text/html - échappe aussi les guillemets dans l'attribut alt des images - bloc de code: passage en largeur au contenu (rendu robuste sous Outlook, ne prend plus toute la largeur), coins arrondis et padding accrus - banner: coins plus arrondis et padding accru Co-Authored-By: Claude Opus 4.8 --- preferences.html | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/preferences.html b/preferences.html index 121f062..1bd279e 100644 --- a/preferences.html +++ b/preferences.html @@ -1537,6 +1537,15 @@ const PG_EXAMPLE_HTML_TPL = { Fonction partagée à l'identique avec la page password-generator. Les {{ placeholders }} sont substitués en dernier (valeurs échappées). ---- */ function emailEsc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); } +/* URL sûre en attribut : whitelist de scheme + échappe les guillemets. + Neutralise javascript:, data:text/html, vbscript:… → « # » si scheme non autorisé. + (emailEsc a déjà neutralisé & < > ; il reste les " qui casseraient l'attribut.) */ +function emailUrl(u){ + const s = String(u == null ? '' : u).trim(); + const sch = /^([a-z][a-z0-9+.\-]*):/i.exec(s); + const ok = !sch || /^(https?|mailto|tel)$/i.test(sch[1]) || /^data:image\//i.test(s); + return (ok ? s : '#').replace(/"/g, '"'); +} function emailFillVars(html, ctx){ return html.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => { if(!(k in ctx)) return m; @@ -1551,7 +1560,7 @@ function renderEmailHtml(src, ctx){ const inline = t => t .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') - .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); + .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (mm, txt, url) => '' + txt + ''); let html = '', i = 0, m; while(i < lines.length){ const line = lines[i]; @@ -1559,18 +1568,18 @@ function renderEmailHtml(src, ctx){ const color = m[1] || '#5b9cf5', buf = []; for(i++; i < lines.length && !/^:::\s*$/.test(lines[i]); i++) buf.push(lines[i]); i++; - html += '
' + inline(buf.join('
')) + '
'; + html += '
' + inline(buf.join('
')) + '
'; continue; } if(/^```/.test(line)){ const buf = []; for(i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]); i++; - html += '
' + buf.join('\n') + '
'; + html += '
' + buf.join('\n') + '
'; continue; } if(m = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/)){ - html += '
' + m[1] + '
'; + html += '
' + m[1].replace(/
'; i++; continue; } if(/^---+\s*$/.test(line)){ html += '
'; i++; continue; }