aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Shoemaker <bracewell@google.com>2026-03-23 13:34:23 -0700
committerGopher Robot <gobot@golang.org>2026-04-08 05:21:28 -0700
commit199c4d1c3c9d509a51f777c81cb17d4b17728097 (patch)
treee7638116713df9a501919cf783fb5e7e6ef8b170
parentd7b6fb44b5f39cb0f551ed7eb62498089b604a88 (diff)
downloadgo-199c4d1c3c9d509a51f777c81cb17d4b17728097.tar.xz
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 <nealpatel@google.com> Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-on: https://go-review.googlesource.com/c/go/+/763762 Reviewed-by: Russ Cox <rsc@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: David Chase <drchase@google.com> Reviewed-by: Fan Mỹ Tâm Club <letrivien97@gmail.com>
-rw-r--r--src/html/template/context.go14
-rw-r--r--src/html/template/escape.go4
-rw-r--r--src/html/template/escape_test.go38
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 43781f38a5..a39d696c42 100644
--- a/src/html/template/escape_test.go
+++ b/src/html/template/escape_test.go
@@ -1186,6 +1186,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`,
},
+ {
+ "<script>var a = `{{if .X}}`{{end}}",
+ `{{if}} branches end in different contexts`,
+ },
+ {
+ "<script>var a = `{{if .X}}a{{else}}`{{end}}",
+ `{{if}} branches end in different contexts`,
+ },
+ {
+ "<script>var a = `{{if .X}}a{{else}}b{{end}}`</script>",
+ ``,
+ },
}
for _, test := range tests {
buf := new(bytes.Buffer)
@@ -1757,7 +1769,7 @@ func TestEscapeText(t *testing.T) {
},
{
"<script>var a = `${",
- context{state: stateJS, element: elementScript},
+ context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${}",
@@ -1765,27 +1777,27 @@ func TestEscapeText(t *testing.T) {
},
{
"<script>var a = `${`",
- context{state: stateJSTmplLit, element: elementScript},
+ context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${var a = \"",
- context{state: stateJSDqStr, element: elementScript},
+ context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${var a = \"`",
- context{state: stateJSDqStr, element: elementScript},
+ context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${var a = \"}",
- context{state: stateJSDqStr, element: elementScript},
+ context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${``",
- context{state: stateJS, element: elementScript},
+ context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${`}",
- context{state: stateJSTmplLit, element: elementScript},
+ context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>`${ {} } asd`</script><script>`${ {} }",
@@ -1793,7 +1805,7 @@ func TestEscapeText(t *testing.T) {
},
{
"<script>var foo = `${ (_ => { return \"x\" })() + \"${",
- context{state: stateJSDqStr, element: elementScript},
+ context{state: stateJSDqStr, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var a = `${ {</script><script>var b = `${ x }",
@@ -1821,23 +1833,23 @@ func TestEscapeText(t *testing.T) {
},
{
"<script>`${ { `` }",
- context{state: stateJS, element: elementScript},
+ context{state: stateJS, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>`${ { }`",
- context{state: stateJSTmplLit, element: elementScript},
+ context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
},
{
"<script>var foo = `${ foo({ a: { c: `${",
- context{state: stateJS, element: elementScript},
+ context{state: stateJS, element: elementScript, jsBraceDepth: []int{2, 0}},
},
{
"<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
- context{state: stateJS, element: elementScript},
+ context{state: stateJS, element: elementScript, jsBraceDepth: []int{1}},
},
{
"<script>`${ `}",
- context{state: stateJSTmplLit, element: elementScript},
+ context{state: stateJSTmplLit, element: elementScript, jsBraceDepth: []int{0}},
},
}