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(`
+ out := policy.Sanitize(` +
badhello
-
`) - for _, want := range []string{"class=\"card\"", "max-width: 600px", "margin: 0 auto", "background: linear-gradient", "box-shadow:", "cellpadding=\"0\"", "cellspacing=\"0\"", "align=\"center\"", "text-align: center"} { +
`) + for _, want := range []string{"

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", "${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, `${mailFrameBaseStyle()}`) + : sanitized.replace(/]*)>/i, `${mailFrameBaseStyle()}`) + return /${withBase}` + } return ` - - -${sanitizedBody} -` +` } function sanitizeComposerHtml(value: string) { return DOMPurify.sanitize(value || "") diff --git a/apps/web/src/types.d.ts b/apps/web/src/types.d.ts index bad997b..543c9ec 100644 --- a/apps/web/src/types.d.ts +++ b/apps/web/src/types.d.ts @@ -2,6 +2,7 @@ declare module "dompurify" { type SanitizeConfig = { ADD_ATTR?: string[] ADD_TAGS?: string[] + WHOLE_DOCUMENT?: boolean } const DOMPurify: { sanitize: (source: string, config?: SanitizeConfig) => string } export default DOMPurify