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>&lt;img src=x onerror=alert(1)&gt;</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>