Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
77 lines
2.6 KiB
Go
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()
|
|
}
|