The vulnerability lies in how text nodes are rendered when they are not in the HTML namespace. The patch introduces a check for the namespace in the render1 function using a new helper function childTextNodesAreLiteral. The render1 function is called by the public Render function. Therefore, both render1 and Render are considered vulnerable in versions prior to the patch. The childTextNodesAreLiteral function is part of the fix and not vulnerable.
The commit 8ffa475fbdb33da97e8bf79cc5791ee8751fca5e in the golang/net repository addresses this vulnerability. The commit message clearly states: "html: only render content literally in the HTML namespace".
The patch modifies html/render.go. The key change is in the render1 function. Before the patch, this function would iterate through child nodes and, for TextNode types, would write the string data directly:
// Old code in render1
// Render any child nodes.
switch n.Data {
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == TextNode {
if _, err := w.WriteString(c.Data); err != nil { // Vulnerable part: c.Data written without escaping
return err
}
// ...
The patched code introduces a call to childTextNodesAreLiteral(n):
// Patched code in render1
// Render any child nodes
if childTextNodesAreLiteral(n) {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == TextNode {
if _, err := w.WriteString(c.Data); err != nil {
return err
}
// ...
} else {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err := render1(w, c); err != nil { // Escaping happens inside the recursive call to render1 for TextNode
return err
}
}
}
The new function childTextNodesAreLiteral checks if the node is in the HTML namespace (n.Namespace != ""). If it's not in the HTML namespace, it returns false, causing the else block in render1 to be executed. In this else block, child nodes are rendered via a recursive call to render1. When render1 is called with a TextNode, it uses escape(w, n.Data) which correctly escapes the text.
// render1 handling of TextNode
case TextNode:
return escape(w, n.Data)
Therefore, the render1 function was the site of the vulnerability. The public Render function is the entry point that calls render1 (via an unexported render function), making it a relevant function that would appear in a stack trace during exploitation.