html: check all ancestor namespace transitions when rendering raw text In childTextNodesAreLiteral, walking up the tree previously stopped at the nearest foreign ancestor once an HTML integration point was found. However, that integration point may itself be a descendant of an element that was foster-parented into a foreign namespace (such as <mglyph> or <malignmark> fostered under <mtext>), causing a parser to resolve the subtree in a different foreign namespace upon re-parsing the rendered output. Continue walking the full ancestor chain and verify that every transition from a non-empty namespace occurs at an HTML integration point before emitting literal text. Thanks to Ali Sherif (https://www.linkedin.com/in/ali-sherif-13812b276) for reporting this issue. Fixes CVE-2026-97045 For golang/go#82622 Change-Id: I67d0006dce9023451905d8e96c4d1f3d05e769cc Reviewed-on: https://go-internal-review.googlesource.com/c/net/+/6360 Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-on: https://go-review.googlesource.com/c/net/+/855121 Reviewed-by: Neal Patel <neal@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Gopher Robot <gobot@golang.org>
diff --git a/html/render.go b/html/render.go index 767eeae..f356d48 100644 --- a/html/render.go +++ b/html/render.go
@@ -246,18 +246,17 @@ 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 + // n's text is literal only if the parser would keep n in the HTML + // namespace when the output is re-parsed. An ancestor may have been + // fostered under a foreign element (such as an HTML <mglyph> under + // <mtext>) where the parser would instead create it, and its + // descendants, in that element's namespace. We walk up the whole tree + // and check that every transition out of a non-HTML namespace happens + // at an HTML integration point. This is stricter than the parser, so + // some text that would be safe literally is escaped. + for c, p := n, n.Parent; p != nil; c, p = p, p.Parent { + if p.Namespace != "" && p.Namespace != c.Namespace && !htmlIntegrationPoint(p) { + return false } }
diff --git a/html/render_test.go b/html/render_test.go index 2de92b2..7066a3b 100644 --- a/html/render_test.go +++ b/html/render_test.go
@@ -207,18 +207,34 @@ } 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) - } + for _, tc := range []struct { + in string + want string + }{ + { + in: `<math><mtext><table><mglyph><style><img src=x onerror=alert(1)>`, + want: "<html><head></head><body><math><mtext><mglyph><style><img src=x onerror=alert(1)></style></mglyph><table></table></mtext></math></body></html>", + }, + { + in: `<math><mtext><table><mglyph><svg><desc><style><img src=x onerror=alert(1)>`, + want: "<html><head></head><body><math><mtext><mglyph><svg><desc><style><img src=x onerror=alert(1)></style></desc></svg></mglyph><table></table></mtext></math></body></html>", + }, + { + in: `<math><mtext><table><malignmark><svg><desc><style><img src=x onerror=alert(1)>`, + want: "<html><head></head><body><math><mtext><malignmark><svg><desc><style><img src=x onerror=alert(1)></style></desc></svg></malignmark><table></table></mtext></math></body></html>", + }, + } { + d, err := Parse(strings.NewReader(tc.in)) + 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) + if buf.String() != tc.want { + t.Errorf("unexpected output for %q:\ngot: %q\nwant: %q", tc.in, buf.String(), tc.want) + } } }