Skip to content

Commit 42f9ee9

Browse files
thevilledevgopherbot
authored andcommitted
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: f4ebd17 GitHub-Pull-Request: #73670 Reviewed-on: https://go-review.googlesource.com/c/go/+/671755 Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Auto-Submit: Sean Liao <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Rob Pike <[email protected]>
1 parent 6425749 commit 42f9ee9

File tree

3 files changed

+37
-0
lines changed

3 files changed

+37
-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. The package 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: 18 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+
stackDepth int // 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,17 @@ const (
4243
SkipFuncCheck // do not check that functions are defined
4344
)
4445

46+
// maxStackDepth is the maximum depth permitted for nested
47+
// parenthesized expressions.
48+
var maxStackDepth = 10000
49+
50+
// init reduces maxStackDepth for WebAssembly due to its smaller stack size.
51+
func init() {
52+
if runtime.GOARCH == "wasm" {
53+
maxStackDepth = 1000
54+
}
55+
}
56+
4557
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
4658
func (t *Tree) Copy() *Tree {
4759
if t == nil {
@@ -223,6 +235,7 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
223235
t.vars = []string{"$"}
224236
t.funcs = funcs
225237
t.treeSet = treeSet
238+
t.stackDepth = 0
226239
lex.options = lexOptions{
227240
emitComment: t.Mode&ParseComments != 0,
228241
breakOK: !t.hasFunction("break"),
@@ -787,6 +800,11 @@ func (t *Tree) term() Node {
787800
}
788801
return number
789802
case itemLeftParen:
803+
if t.stackDepth >= maxStackDepth {
804+
t.errorf("max expression depth exceeded")
805+
}
806+
t.stackDepth++
807+
defer func() { t.stackDepth-- }()
790808
return t.pipeline("parenthesized pipeline", itemRightParen)
791809
case itemString, itemRawString:
792810
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
@@ -86,6 +86,11 @@ var numberTests = []numberTest{
8686
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
8787
}
8888

89+
func init() {
90+
// Use a small stack limit for testing to avoid creating huge expressions.
91+
maxStackDepth = 3
92+
}
93+
8994
func TestNumberParse(t *testing.T) {
9095
for _, test := range numberTests {
9196
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
@@ -327,6 +332,15 @@ var parseTests = []parseTest{
327332
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
328333
// Missing pipeline in block
329334
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
335+
336+
// Expression nested depth tests.
337+
{"paren nesting normal", "{{ (( 1 )) }}", noError, "{{((1))}}"},
338+
{"paren nesting at limit", "{{ ((( 1 ))) }}", noError, "{{(((1)))}}"},
339+
{"paren nesting exceeds limit", "{{ (((( 1 )))) }}", hasError, "template: test:1: max expression depth exceeded"},
340+
{"paren nesting in pipeline", "{{ ((( 1 ))) | printf }}", noError, "{{(((1))) | printf}}"},
341+
{"paren nesting in pipeline exceeds limit", "{{ (((( 1 )))) | printf }}", hasError, "template: test:1: max expression depth exceeded"},
342+
{"paren nesting with other constructs", "{{ if ((( true ))) }}YES{{ end }}", noError, "{{if (((true)))}}\"YES\"{{end}}"},
343+
{"paren nesting with other constructs exceeds limit", "{{ if (((( true )))) }}YES{{ end }}", hasError, "template: test:1: max expression depth exceeded"},
330344
}
331345

332346
var builtins = map[string]any{

0 commit comments

Comments
 (0)