package app import ( "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "regexp" "strings" "time" "unicode" "github.com/microcosm-cc/bluemonday" ) type HTMLPolicy struct{ policy *bluemonday.Policy } func NewHTMLPolicy() *HTMLPolicy { p := bluemonday.UGCPolicy() p.AllowElements("html", "head", "body", "center", "font") p.AllowAttrs("style").Globally() p.AllowAttrs("class").Matching(bluemonday.SpaceSeparatedTokens).Globally() p.AllowAttrs("align", "valign").Matching(bluemonday.Paragraph).Globally() p.AllowAttrs("width", "height").Matching(bluemonday.NumberOrPercent).Globally() p.AllowAttrs("bgcolor", "color").Matching(regexp.MustCompile(`(?i)^#[0-9a-f]{3,8}$|^[a-z][a-z0-9 -]{0,31}$`)).Globally() p.AllowAttrs("border", "cellpadding", "cellspacing").Matching(bluemonday.Number).OnElements("table") p.AllowStyles( "background", "background-color", "background-image", "border", "border-collapse", "border-color", "border-radius", "border-spacing", "border-style", "border-width", "box-shadow", "color", "display", "font", "font-family", "font-size", "font-style", "font-weight", "height", "letter-spacing", "line-height", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "max-width", "min-width", "opacity", "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", "text-align", "text-decoration", "text-transform", "vertical-align", "white-space", "width", ).MatchingHandler(safeEmailCSSValue).Globally() return &HTMLPolicy{policy: p} } func (p *HTMLPolicy) Sanitize(s string) string { if p == nil || p.policy == nil { return s } styles, withoutStyles := extractSafeEmailStyles(s) clean := p.policy.Sanitize(withoutStyles) if len(styles) == 0 { return clean } return strings.Join(styles, "") + clean } var emailStyleTagRe = regexp.MustCompile(`(?is)]*)>(.*?)`) var htmlNonContentTagRe = regexp.MustCompile(`(?is)<(style|script|head|title|noscript)\b[^>]*>.*?`) 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", " 512 { return false } unsafe := []string{"expression", "javascript:", "vbscript:", "data:", "behavior", "-moz-binding", "@import", " 160 { r := []rune(s) s = string(r[:160]) + "…" } return s } func stripTags(s string) string { s = htmlNonContentTagRe.ReplaceAllString(s, " ") var b strings.Builder inTag := false for _, r := range s { switch r { case '<': inTag = true case '>': inTag = false default: if !inTag { if unicode.IsSpace(r) { b.WriteRune(' ') } else { b.WriteRune(r) } } } } return strings.Join(strings.Fields(b.String()), " ") } func badRequest(w http.ResponseWriter, err error) { msg := "bad request" if err != nil { msg = err.Error() } respondError(w, http.StatusBadRequest, msg) } func requireString(name, value string) error { if strings.TrimSpace(value) == "" { return fmt.Errorf("%s is required", name) } return nil } var errNotFound = errors.New("not found")