text/template: limit expression parenthesis nesting

Deeply nested parenthesized expressions could cause a stack
overflow during parsing. This change introduces a depth limit
(maxStackDepth) tracked in Tree.stackDepth to prevent this.

Additionally, this commit clarifies the security model in
the package documentation, noting that template authors
are trusted as text/template does not auto-escape.

Fixes #71201

Change-Id: Iab2c2ea6c193ceb44bb2bc7554f3fccf99a9542f
GitHub-Last-Rev: f4ebd1719f
GitHub-Pull-Request: golang/go#73670
Reviewed-on: https://go-review.googlesource.com/c/go/+/671755
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Auto-Submit: Sean Liao <sean@liao.dev>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Rob Pike <r@golang.org>
This commit is contained in:
Ville Vesilehto 2025-05-14 18:16:54 +00:00 committed by Gopher Robot
parent 6425749695
commit 42f9ee904c
3 changed files with 37 additions and 0 deletions

View File

@ -15,6 +15,11 @@ Execution of the template walks the structure and sets the cursor, represented
by a period '.' and called "dot", to the value at the current location in the
structure as execution proceeds.
The security model used by this package assumes that template authors are
trusted. The package does not auto-escape output, so injecting code into
a template can lead to arbitrary code execution if the template is executed
by an untrusted source.
The input text for a template is UTF-8-encoded text in any format.
"Actions"--data evaluations or control structures--are delimited by
"{{" and "}}"; all text outside actions is copied to the output unchanged.

View File

@ -32,6 +32,7 @@ type Tree struct {
treeSet map[string]*Tree
actionLine int // line of left delim starting action
rangeDepth int
stackDepth int // depth of nested parenthesized expressions
}
// A mode value is a set of flags (or 0). Modes control parser behavior.
@ -42,6 +43,17 @@ const (
SkipFuncCheck // do not check that functions are defined
)
// maxStackDepth is the maximum depth permitted for nested
// parenthesized expressions.
var maxStackDepth = 10000
// init reduces maxStackDepth for WebAssembly due to its smaller stack size.
func init() {
if runtime.GOARCH == "wasm" {
maxStackDepth = 1000
}
}
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
@ -223,6 +235,7 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
t.vars = []string{"$"}
t.funcs = funcs
t.treeSet = treeSet
t.stackDepth = 0
lex.options = lexOptions{
emitComment: t.Mode&ParseComments != 0,
breakOK: !t.hasFunction("break"),
@ -787,6 +800,11 @@ func (t *Tree) term() Node {
}
return number
case itemLeftParen:
if t.stackDepth >= maxStackDepth {
t.errorf("max expression depth exceeded")
}
t.stackDepth++
defer func() { t.stackDepth-- }()
return t.pipeline("parenthesized pipeline", itemRightParen)
case itemString, itemRawString:
s, err := strconv.Unquote(token.val)

View File

@ -86,6 +86,11 @@ var numberTests = []numberTest{
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
}
func init() {
// Use a small stack limit for testing to avoid creating huge expressions.
maxStackDepth = 3
}
func TestNumberParse(t *testing.T) {
for _, test := range numberTests {
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
@ -327,6 +332,15 @@ var parseTests = []parseTest{
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
// Missing pipeline in block
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
// Expression nested depth tests.
{"paren nesting normal", "{{ (( 1 )) }}", noError, "{{((1))}}"},
{"paren nesting at limit", "{{ ((( 1 ))) }}", noError, "{{(((1)))}}"},
{"paren nesting exceeds limit", "{{ (((( 1 )))) }}", hasError, "template: test:1: max expression depth exceeded"},
{"paren nesting in pipeline", "{{ ((( 1 ))) | printf }}", noError, "{{(((1))) | printf}}"},
{"paren nesting in pipeline exceeds limit", "{{ (((( 1 )))) | printf }}", hasError, "template: test:1: max expression depth exceeded"},
{"paren nesting with other constructs", "{{ if ((( true ))) }}YES{{ end }}", noError, "{{if (((true)))}}\"YES\"{{end}}"},
{"paren nesting with other constructs exceeds limit", "{{ if (((( true )))) }}YES{{ end }}", hasError, "template: test:1: max expression depth exceeded"},
}
var builtins = map[string]any{