80 lines
2.5 KiB
Go
80 lines
2.5 KiB
Go
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 <img src="..."> 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
|
|
}
|