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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user