Skip to content

Commit 6751d67

Browse files
committed
text/template: limit expression parenthesis nesting
Deeply nested parenthesized expressions could cause a stack overflow during parsing. This change introduces a depth limit (maxExpressionParenDepth) tracked in Tree.parenDepth 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 golang#71201 Signed-off-by: Ville Vesilehto <[email protected]>
1 parent e282cbb commit 6751d67

File tree

3 files changed

+30
-0
lines changed

3 files changed

+30
-0
lines changed

src/text/template/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ Execution of the template walks the structure and sets the cursor, represented
1515
by a period '.' and called "dot", to the value at the current location in the
1616
structure as execution proceeds.
1717
18+
The security model used by this package assumes that template authors are
19+
trusted. text/template does not auto-escape output, so injecting code into
20+
a template can lead to arbitrary code execution if the template is executed
21+
by an untrusted source.
22+
1823
The input text for a template is UTF-8-encoded text in any format.
1924
"Actions"--data evaluations or control structures--are delimited by
2025
"{{" and "}}"; all text outside actions is copied to the output unchanged.

src/text/template/parse/parse.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Tree struct {
3232
treeSet map[string]*Tree
3333
actionLine int // line of left delim starting action
3434
rangeDepth int
35+
parenDepth int // current depth of nested parenthesized expressions
3536
}
3637

3738
// A mode value is a set of flags (or 0). Modes control parser behavior.
@@ -42,6 +43,10 @@ const (
4243
SkipFuncCheck // do not check that functions are defined
4344
)
4445

46+
// maxExpressionParenDepth is the maximum depth of nested parenthesized expressions.
47+
// It is used to prevent stack overflows from deep finite recursion in the parser.
48+
const maxExpressionParenDepth = 10000
49+
4550
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
4651
func (t *Tree) Copy() *Tree {
4752
if t == nil {
@@ -223,6 +228,7 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
223228
t.vars = []string{"$"}
224229
t.funcs = funcs
225230
t.treeSet = treeSet
231+
t.parenDepth = 0
226232
lex.options = lexOptions{
227233
emitComment: t.Mode&ParseComments != 0,
228234
breakOK: !t.hasFunction("break"),
@@ -787,6 +793,11 @@ func (t *Tree) term() Node {
787793
}
788794
return number
789795
case itemLeftParen:
796+
if t.parenDepth >= maxExpressionParenDepth {
797+
t.errorf("max expression depth exceeded")
798+
}
799+
t.parenDepth++
800+
defer func() { t.parenDepth-- }()
790801
return t.pipeline("parenthesized pipeline", itemRightParen)
791802
case itemString, itemRawString:
792803
s, err := strconv.Unquote(token.val)

src/text/template/parse/parse_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,15 @@ var parseTests = []parseTest{
327327
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
328328
// Missing pipeline in block
329329
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
330+
331+
// Parenthesis nesting depth tests
332+
{"paren nesting normal", "{{( ( ( ( (1) ) ) ) )}}", noError, "{{(((((1)))))}}"},
333+
{"paren nesting at limit", "{{" + buildNestedParenExpression(10000, "1") + "}}", noError, "{{" + buildNestedParenExpression(10000, "1") + "}}"},
334+
{"paren nesting exceeds limit", "{{" + buildNestedParenExpression(10001, "1") + "}}", hasError, "template: test:1: max expression depth exceeded"},
335+
{"paren nesting in pipeline", "{{ ( ( ( ( (1) ) ) ) ) | printf }}", noError, "{{(((((1))))) | printf}}"},
336+
{"paren nesting in pipeline exceeds limit", "{{ " + buildNestedParenExpression(10001, "1") + " | printf }}", hasError, "template: test:1: max expression depth exceeded"},
337+
{"paren nesting with other constructs", "{{if " + buildNestedParenExpression(5, "true") + "}}YES{{end}}", noError, "{{if " + buildNestedParenExpression(5, "true") + "}}\"YES\"{{end}}"},
338+
{"paren nesting with other constructs exceeds limit", "{{if " + buildNestedParenExpression(10001, "true") + "}}YES{{end}}", hasError, "template: test:1: max expression depth exceeded"},
330339
}
331340

332341
var builtins = map[string]any{
@@ -716,3 +725,8 @@ func BenchmarkListString(b *testing.B) {
716725
b.Fatal("Benchmark was not run")
717726
}
718727
}
728+
729+
// buildNestedParenExpression is a helper for testing parenthesis depth.
730+
func buildNestedParenExpression(depth int, content string) string {
731+
return strings.Repeat("(", depth) + content + strings.Repeat(")", depth)
732+
}

0 commit comments

Comments
 (0)