Files
descrybe/apps/api/internal/security/html.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

77 lines
2.6 KiB
Go

package security
import (
"strings"
"unicode"
"github.com/microcosm-cc/bluemonday"
)
const MaxEmailHTMLRunes = 500_000
// emailHTMLPolicy is a parser-grade allowlist for campaign/email HTML.
// It strips scripts, event handlers, javascript:/data: URLs, and high-risk tags
// while preserving a safe email HTML subset (tables, typography, remote images).
var emailHTMLPolicy = newEmailHTMLPolicy()
func newEmailHTMLPolicy() *bluemonday.Policy {
p := bluemonday.UGCPolicy()
// Layout tags common in email HTML (beyond UGC defaults).
p.AllowElements(
"div", "table", "thead", "tbody", "tfoot", "tr", "th", "td",
"caption", "colgroup", "col", "center", "font",
)
p.AllowAttrs(
"width", "height", "align", "valign", "bgcolor",
"cellpadding", "cellspacing", "border", "colspan", "rowspan", "role",
).OnElements("table", "tr", "td", "th", "thead", "tbody", "tfoot", "col", "colgroup", "div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "center")
p.AllowAttrs("color", "face", "size").OnElements("font")
p.AllowAttrs("span").OnElements("col", "colgroup")
// Safe inline CSS only — style tags and event handlers remain disallowed.
p.AllowAttrs("style").Globally()
p.AllowStyles(
"background-color", "color",
"font-family", "font-size", "font-weight", "font-style",
"text-align", "text-decoration", "line-height", "letter-spacing",
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
"border", "border-top", "border-right", "border-bottom", "border-left",
"border-color", "border-style", "border-width", "border-collapse", "border-spacing",
"width", "height", "max-width", "min-width", "max-height", "min-height",
"display", "vertical-align", "white-space",
).Globally()
// http(s)/mailto only; data: images stay off (do not call AllowDataURIImages).
// Reject relative URLs that could be redirected via a stripped <base>.
p.AllowRelativeURLs(false)
p.RequireParseableURLs(true)
p.AllowURLSchemes("http", "https", "mailto")
return p
}
// SanitizeEmailHTML removes high-risk tags/attrs from generated campaign HTML before store/send.
// Parser-grade allowlist (bluemonday). Empty input stays empty.
func SanitizeEmailHTML(html string) string {
html = strings.TrimSpace(html)
if html == "" {
return ""
}
html = stripControlsKeepNewlines(html)
html = emailHTMLPolicy.Sanitize(html)
return TruncateRunes(html, MaxEmailHTMLRunes)
}
func stripControlsKeepNewlines(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r == '\n' || r == '\r' || r == '\t' || unicode.IsPrint(r) {
b.WriteRune(r)
}
}
return b.String()
}