diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index d097385..701d7a7 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -842,16 +842,21 @@ func TestCatchAllStoresUnregisteredMailForAdminOnly(t *testing.T) { func TestHTMLPolicyPreservesEmailLayoutStyles(t *testing.T) { policy := NewHTMLPolicy() - out := policy.Sanitize(`
|
bad |
ok
`) + if strings.Contains(blockedOut, "`) + +func extractSafeEmailStyles(value string) ([]string, string) { + styles := []string{} + withoutStyles := emailStyleTagRe.ReplaceAllStringFunc(value, func(tag string) string { + match := emailStyleTagRe.FindStringSubmatch(tag) + if len(match) != 3 { + return "" + } + attrs, css := match[1], strings.TrimSpace(match[2]) + if !safeEmailStyleAttrs(attrs) || !safeEmailCSSBlock(css) { + return "" + } + styles = append(styles, ``) + return "" + }) + return styles, withoutStyles +} + +func safeEmailStyleAttrs(attrs string) bool { + attrs = strings.ToLower(strings.TrimSpace(attrs)) + if attrs == "" { + return true + } + return regexp.MustCompile(`^\s*type\s*=\s*["']?text/css["']?\s*$`).MatchString(attrs) +} + +func safeEmailCSSBlock(value string) bool { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" || len(value) > 50000 { + return false + } + unsafe := []string{"expression", "javascript:", "vbscript:", "data:", "behavior", "-moz-binding", "@import", "", "url("} + for _, token := range unsafe { + if strings.Contains(value, token) { + return false + } + } + return true } func safeEmailCSSValue(value string) bool { diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index 65e7c63..5fbf57a 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -3114,17 +3114,31 @@ function escapeHtml(value: string) { } function buildMailFrameSrcDoc(bodyHtml: string, bodyText: string) { const rawBody = bodyHtml.trim() ? bodyHtml : `${escapeHtml(bodyText || "")}`
- const sanitizedBody = DOMPurify.sanitize(rawBody, {
- ADD_ATTR: ["style", "align", "valign", "bgcolor", "border", "cellpadding", "cellspacing", "width", "height"],
- ADD_TAGS: ["center"],
+ const sanitized = DOMPurify.sanitize(rawBody, {
+ ADD_ATTR: ["style", "type", "align", "valign", "bgcolor", "border", "cellpadding", "cellspacing", "width", "height"],
+ ADD_TAGS: ["html", "head", "body", "style", "center", "font"],
+ WHOLE_DOCUMENT: /]/i.test(rawBody) || /]/i.test(rawBody),
})
+ if (/]/i.test(sanitized) || /]/i.test(sanitized)) {
+ const hasHead = /]/i.test(sanitized)
+ const withBase = hasHead
+ ? sanitized.replace(/]*)>/i, `