html: properly render fostered elements in foreign content When we foster elements under another parent, there are complicated rules about which namespace may apply. This in particular affects childTextNodesAreLiteral, which checks if we should be emitting raw text, or escaped text. In childTextNodesAreLiteral, check if there is an ancestor which has a different namespace. If one is found, check if it's an HTML integration point. If not, treat the node as if it were in its parents namespace, if so, treat it as HTML. Thanks to Tristan Madani for reporting this issue. Fixes CVE-2026-42502 Change-Id: I0ae1780dae335e5f719d7f176cefa83670cfea3d Reviewed-on: https://go-review.googlesource.com/c/net/+/781701 Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Nicholas Husin <nsh@golang.org> TryBot-Bypass: Roland Shoemaker <roland@golang.org> Reviewed-by: Nicholas Husin <husin@google.com> Auto-Submit: Gopher Robot <gobot@golang.org>
diff --git a/html/parse_test.go b/html/parse_test.go index fe66eb4..cafa34d 100644 --- a/html/parse_test.go +++ b/html/parse_test.go
@@ -449,6 +449,8 @@ `<table><math><select><mi><select></table>`: true, `<!doctype html><table><colgroup><plaintext></plaintext>`: true, `<!doctype html><svg><plaintext>a</plaintext>b`: true, + // Due to fostering, parsing the rendered output produces a different tree. + `<math><mtext><table><mglyph><style><img>`: true, } func TestNodeConsistency(t *testing.T) {
diff --git a/html/render.go b/html/render.go index 0157d89..418fed0 100644 --- a/html/render.go +++ b/html/render.go
@@ -243,8 +243,24 @@ if n.Namespace != "" { return false } + switch n.Data { case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp": + // We need to check if n is a node that was fostered from a HTML namespace + // into a non-HTML namespace (in which case, different rules apply to it). + // We do this by walking up the tree until we find a node with a non-empty + // namespace. If we find such a node, we also have to check if it's + // an HTML integration point. If it isn't, then the node we're currently + // looking at is foster-parented and we should return false. + for p := n.Parent; p != nil; p = p.Parent { + if p.Namespace != "" { + if !htmlIntegrationPoint(p) { + return false + } + break + } + } + return true default: return false
diff --git a/html/render_test.go b/html/render_test.go index 22d0864..2de92b2 100644 --- a/html/render_test.go +++ b/html/render_test.go
@@ -205,3 +205,20 @@ } } } + +func TestRenderFosteredForeignContent(t *testing.T) { + a := `<math><mtext><table><mglyph><style><img src=x onerror=alert(1)>` + d, err := Parse(strings.NewReader(a)) + if err != nil { + t.Fatal(err) + } + buf := bytes.NewBuffer(nil) + if err := Render(buf, d); err != nil { + t.Fatal(err) + } + + expected := "<html><head></head><body><math><mtext><mglyph><style><img src=x onerror=alert(1)></style></mglyph><table></table></mtext></math></body></html>" + if buf.String() != expected { + t.Errorf("unexpected output: got %q, want %q", buf.String(), expected) + } +}
diff --git a/html/testdata/go/fostered_foreign_content.dat b/html/testdata/go/fostered_foreign_content.dat new file mode 100644 index 0000000..2c0cd72 --- /dev/null +++ b/html/testdata/go/fostered_foreign_content.dat
@@ -0,0 +1,13 @@ +#data +<math><mtext><table><mglyph><style><img> +#errors +#document +| <html> +| <head> +| <body> +| <math math> +| <math mtext> +| <mglyph> +| <style> +| "<img>" +| <table>