From 3ed316924408a02b256544eb40607e73702f2d0c Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Mon, 23 Mar 2026 13:34:23 -0700 Subject: [release-branch.go1.25] html/template: properly track JS template literal brace depth across contexts Properly track JS template literal brace depth across branches/ranges, and prevent accidental re-use of escape analysis by including the brace depth in the stringification/mangling for contexts. Fixes #78331 Fixes CVE-2026-32289 Change-Id: I9f3f47c29e042220b18e4d3299db7a3fae4207fa Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/3882 Reviewed-by: Neal Patel Reviewed-by: Nicholas Husin Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/4000 Reviewed-by: Damien Neil Commit-Queue: Damien Neil Reviewed-on: https://go-review.googlesource.com/c/go/+/763551 Auto-Submit: Gopher Robot Reviewed-by: Junyang Shao Reviewed-by: David Chase TryBot-Bypass: Gopher Robot --- src/html/template/context.go | 14 +++++++++++++- src/html/template/escape.go | 4 ++-- src/html/template/escape_test.go | 38 +++++++++++++++++++++++++------------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/html/template/context.go b/src/html/template/context.go index 8b3af2feab..132ae2d28d 100644 --- a/src/html/template/context.go +++ b/src/html/template/context.go @@ -6,6 +6,7 @@ package template import ( "fmt" + "slices" "text/template/parse" ) @@ -37,7 +38,7 @@ func (c context) String() string { if c.err != nil { err = c.err } - return fmt.Sprintf("{%v %v %v %v %v %v %v}", c.state, c.delim, c.urlPart, c.jsCtx, c.attr, c.element, err) + return fmt.Sprintf("{%v %v %v %v %v %v %v %v}", c.state, c.delim, c.urlPart, c.jsCtx, c.jsBraceDepth, c.attr, c.element, err) } // eq reports whether two contexts are equal. @@ -46,6 +47,7 @@ func (c context) eq(d context) bool { c.delim == d.delim && c.urlPart == d.urlPart && c.jsCtx == d.jsCtx && + slices.Equal(c.jsBraceDepth, d.jsBraceDepth) && c.attr == d.attr && c.element == d.element && c.err == d.err @@ -68,6 +70,9 @@ func (c context) mangle(templateName string) string { if c.jsCtx != jsCtxRegexp { s += "_" + c.jsCtx.String() } + if c.jsBraceDepth != nil { + s += fmt.Sprintf("_jsBraceDepth(%v)", c.jsBraceDepth) + } if c.attr != attrNone { s += "_" + c.attr.String() } @@ -77,6 +82,13 @@ func (c context) mangle(templateName string) string { return s } +// clone returns a copy of c with the same field values. +func (c context) clone() context { + clone := c + clone.jsBraceDepth = slices.Clone(c.jsBraceDepth) + return clone +} + // state describes a high-level HTML parser state. // // It bounds the top of the element stack, and by extension the HTML insertion diff --git a/src/html/template/escape.go b/src/html/template/escape.go index d8e1b8cb54..e18fa3aa73 100644 --- a/src/html/template/escape.go +++ b/src/html/template/escape.go @@ -523,7 +523,7 @@ func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) if nodeName == "range" { e.rangeContext = &rangeContext{outer: e.rangeContext} } - c0 := e.escapeList(c, n.List) + c0 := e.escapeList(c.clone(), n.List) if nodeName == "range" { if c0.state != stateError { c0 = joinRange(c0, e.rangeContext) @@ -554,7 +554,7 @@ func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) return c0 } } - c1 := e.escapeList(c, n.ElseList) + c1 := e.escapeList(c.clone(), n.ElseList) return join(c0, c1, n, nodeName) } diff --git a/src/html/template/escape_test.go b/src/html/template/escape_test.go index 49710c38b7..126dc22f33 100644 --- a/src/html/template/escape_test.go +++ b/src/html/template/escape_test.go @@ -1185,6 +1185,18 @@ func TestErrors(t *testing.T) { // html is allowed since it is the last command in the pipeline, but urlquery is not. `predefined escaper "urlquery" disallowed in template`, }, + { + "", + ``, + }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1756,7 +1768,7 @@ func TestEscapeText(t *testing.T) { }, { "