package webui import ( "strings" "golang.org/x/net/html" ) // stripRemoteImages walks already-sanitized HTML (htmlBodyPolicy has already removed // scripts/event handlers/etc. — this never runs on untrusted-for-XSS-purposes input) // and neutralizes any that isn't a data: URI, renaming it to // data-blocked-src so the browser never fetches it — a classic tracking-pixel/ // read-receipt vector otherwise. Reports whether anything was actually blocked, so // the caller only shows a "Show images" banner when there's something to reveal. // // This runs as a second pass over the DOM rather than trying to make bluemonday's own // policy conditionally reject remote img src — bluemonday composes URL-scheme rules // globally per policy (AllowStandardURLs), and UGCPolicy already bakes in "img src // follows the global scheme allowlist" internally, so cleanly restricting only img // src to data: URIs while leaving other elements' href/src alone isn't something the // policy API exposes directly. A dedicated pass keeps the two concerns (XSS // sanitization vs. privacy-motivated image blocking) independent and easy to reason // about separately. func stripRemoteImages(sanitizedHTML string) (cleaned string, blocked bool) { doc, err := html.Parse(strings.NewReader(sanitizedHTML)) if err != nil { return sanitizedHTML, false } var walk func(*html.Node) walk = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "img" { for i, attr := range n.Attr { if attr.Key != "src" { continue } if strings.HasPrefix(attr.Val, "data:") { break } n.Attr[i].Key = "data-blocked-src" blocked = true break } } for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c) } } walk(doc) if !blocked { return sanitizedHTML, false } var buf strings.Builder // html.Parse wraps a fragment in a full document (html>head,body) — render just // the body's children back out, matching what was originally passed in (a // fragment, not a full document). body := findBody(doc) if body == nil { return sanitizedHTML, false } for c := body.FirstChild; c != nil; c = c.NextSibling { if err := html.Render(&buf, c); err != nil { return sanitizedHTML, false } } return buf.String(), true } func findBody(n *html.Node) *html.Node { if n.Type == html.ElementNode && n.Data == "body" { return n } for c := n.FirstChild; c != nil; c = c.NextSibling { if b := findBody(c); b != nil { return b } } return nil }