mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-18 09:27:53 +00:00
Template execution rework
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
diff -ruN a/exec.go b/exec.go
|
||||
--- a/exec.go 2026-07-08 15:38:43.551040811 +0200
|
||||
+++ b/exec.go 2026-07-08 15:38:43.553556913 +0200
|
||||
@@ -2,17 +2,19 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-package template
|
||||
+package gotext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
- "internal/fmtsort"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template/parse"
|
||||
+ "time"
|
||||
+
|
||||
+ "heckel.io/ntfy/v2/template/gotext/fmtsort"
|
||||
)
|
||||
|
||||
// maxExecDepth specifies the maximum stack depth of templates within
|
||||
@@ -32,11 +34,13 @@
|
||||
// template so that multiple executions of the same template
|
||||
// can execute in parallel.
|
||||
type state struct {
|
||||
- tmpl *Template
|
||||
- wr io.Writer
|
||||
- node parse.Node // current node, for errors
|
||||
- vars []variable // push-down stack of variable values.
|
||||
- depth int // the height of the stack of executing templates.
|
||||
+ tmpl *Template
|
||||
+ wr io.Writer
|
||||
+ node parse.Node // current node, for errors
|
||||
+ vars []variable // push-down stack of variable values.
|
||||
+ depth int // the height of the stack of executing templates.
|
||||
+ deadline time.Time // ntfy: wall-clock bail-out; zero means no limit
|
||||
+ steps int64 // ntfy: node counter for amortized deadline checks
|
||||
}
|
||||
|
||||
// variable holds the dynamic value of a variable such as $, $x etc.
|
||||
@@ -131,6 +135,10 @@
|
||||
return e.Err
|
||||
}
|
||||
|
||||
+// ErrExecutionInterrupted is wrapped into the error returned by Execute when a template exceeds the
|
||||
+// deadline set via Template.SetExecutionDeadline. Detect it with errors.Is. (ntfy addition)
|
||||
+var ErrExecutionInterrupted = errors.New("template execution interrupted")
|
||||
+
|
||||
// errorf records an ExecError and terminates processing.
|
||||
func (s *state) errorf(format string, args ...any) {
|
||||
name := doublePercent(s.tmpl.Name())
|
||||
@@ -214,9 +222,10 @@
|
||||
value = reflect.ValueOf(data)
|
||||
}
|
||||
state := &state{
|
||||
- tmpl: t,
|
||||
- wr: wr,
|
||||
- vars: []variable{{"$", value}},
|
||||
+ tmpl: t,
|
||||
+ wr: wr,
|
||||
+ vars: []variable{{"$", value}},
|
||||
+ deadline: t.deadline, // ntfy: wall-clock execution bail-out
|
||||
}
|
||||
if t.Tree == nil || t.Root == nil {
|
||||
state.errorf("%q is an incomplete or empty template", t.Name())
|
||||
@@ -260,6 +269,11 @@
|
||||
// generating output as they go.
|
||||
func (s *state) walk(dot reflect.Value, node parse.Node) {
|
||||
s.at(node)
|
||||
+ // ntfy: amortized wall-clock bail-out to prevent CPU DoS from user-supplied templates
|
||||
+ // (tight/nested ranges that never write output). See GHSA-rhwf-xgc9-m9fp.
|
||||
+ if s.steps++; s.steps&0xff == 0 && !s.deadline.IsZero() && time.Now().After(s.deadline) {
|
||||
+ s.errorf("execution interrupted: %w", ErrExecutionInterrupted)
|
||||
+ }
|
||||
switch node := node.(type) {
|
||||
case *parse.ActionNode:
|
||||
// Do not pop variables so they persist until next end.
|
||||
diff -ruN a/funcs.go b/funcs.go
|
||||
--- a/funcs.go 2026-07-08 15:38:43.551127782 +0200
|
||||
+++ b/funcs.go 2026-07-08 15:38:43.553627333 +0200
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-package template
|
||||
+package gotext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
diff -ruN a/option.go b/option.go
|
||||
--- a/option.go 2026-07-08 15:38:43.551232856 +0200
|
||||
+++ b/option.go 2026-07-08 15:38:43.553688366 +0200
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// This file contains the code to handle template options.
|
||||
|
||||
-package template
|
||||
+package gotext
|
||||
|
||||
import "strings"
|
||||
|
||||
diff -ruN a/template.go b/template.go
|
||||
--- a/template.go 2026-07-08 15:38:43.551182684 +0200
|
||||
+++ b/template.go 2026-07-08 15:38:43.553660525 +0200
|
||||
@@ -2,20 +2,22 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-package template
|
||||
+package gotext
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"reflect"
|
||||
"sync"
|
||||
"text/template/parse"
|
||||
+ "time"
|
||||
)
|
||||
|
||||
// common holds the information shared by related templates.
|
||||
type common struct {
|
||||
- tmpl map[string]*Template // Map from name to defined templates.
|
||||
- muTmpl sync.RWMutex // protects tmpl
|
||||
- option option
|
||||
+ tmpl map[string]*Template // Map from name to defined templates.
|
||||
+ muTmpl sync.RWMutex // protects tmpl
|
||||
+ option option
|
||||
+ deadline time.Time // ntfy: wall-clock execution deadline (zero = none)
|
||||
// We use two maps, one for parsing and one for execution.
|
||||
// This separation makes the API cleaner since it doesn't
|
||||
// expose reflection to the client.
|
||||
@@ -49,6 +51,15 @@
|
||||
return t.name
|
||||
}
|
||||
|
||||
+// SetExecutionDeadline sets a wall-clock deadline after which Execute aborts with an error wrapping
|
||||
+// ErrExecutionInterrupted. A zero deadline disables the limit. It bounds CPU for untrusted templates
|
||||
+// that text/template cannot otherwise interrupt. (ntfy addition, see GHSA-rhwf-xgc9-m9fp.)
|
||||
+func (t *Template) SetExecutionDeadline(deadline time.Time) *Template {
|
||||
+ t.init()
|
||||
+ t.deadline = deadline
|
||||
+ return t
|
||||
+}
|
||||
+
|
||||
// New allocates a new, undefined template associated with the given one and with the same
|
||||
// delimiters. The association, which is transitive, allows one template to
|
||||
// invoke another with a {{template}} action.
|
||||
Reference in New Issue
Block a user