diff options
| author | Roland Shoemaker <bracewell@google.com> | 2026-03-23 13:34:23 -0700 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2026-04-08 05:21:28 -0700 |
| commit | 199c4d1c3c9d509a51f777c81cb17d4b17728097 (patch) | |
| tree | e7638116713df9a501919cf783fb5e7e6ef8b170 | |
| parent | d7b6fb44b5f39cb0f551ed7eb62498089b604a88 (diff) | |
| download | go-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.go | 14 | ||||
| -rw-r--r-- | src/html/template/escape.go | 4 | ||||
| -rw-r--r-- | 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 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}}, }, } |
