fix(template): gate cmd/readFile/stat/glob behind trusted templates

template.Render's func map exposed cmd/readFile/stat/glob/env/expandenv
to any string passed as the template argument, with no way to tell a
template the user authored in their config from one built at runtime
out of external data (filesystem names, command output, ...). That
distinction is exactly what let a malicious folder name reach cmd
(fixed for that one call site in a prior commit); nothing stopped the
same class of bug from reappearing at a future call site.

Split Render into two explicitly named functions instead: RenderTrusted
keeps the full func map and is for template text read verbatim from a
config field (segment/block/palette templates, mapped_locations keys,
folder_separator_template, ...) — every existing call site converts to
it. RenderUntrusted drops cmd/readFile/stat/glob/env/expandenv and is
for text that may contain or be composed from runtime data; pt.Path in
path.go's setStyle(), the one sink that re-renders a string composed
from raw filesystem folder names, uses it. Neither name is shorter or
more "default" than the other, so there's no ambient plain Render a
future call site could reach for without first deciding which one it
means. The parsed-template cache key includes the trust level so a
trusted and an untrusted render of identical text can never share a
cached *template.Template and its func map.

Entire-Checkpoint: 597e5a60d968
This commit is contained in:
Jan De Dobbeleer
2026-07-23 12:44:26 +02:00
committed by Jan De Dobbeleer
parent 7ec9fb8eb0
commit 88ddbe0b0a
30 changed files with 171 additions and 74 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ func (c Ansi) ResolveTemplate() Ansi {
return emptyColor
}
text, err := template.Render(string(c), nil)
text, err := template.RenderTrusted(string(c), nil)
if err != nil {
return Transparent
}
+1 -1
View File
@@ -37,7 +37,7 @@ func (p Palette) resolveColor(colorName Ansi, depth int, originalColorName *Ansi
}
if strings.Contains(color.String(), "{{") {
rendered, err := template.Render(color.String(), nil)
rendered, err := template.RenderTrusted(color.String(), nil)
if err != nil {
return "", err
}
+1 -1
View File
@@ -98,7 +98,7 @@ func (cfg *Config) getPalette() color.Palette {
return cfg.Palette
}
key, err := template.Render(cfg.Palettes.Template, nil)
key, err := template.RenderTrusted(cfg.Palettes.Template, nil)
if err != nil {
return cfg.Palette
}
+3 -3
View File
@@ -27,7 +27,7 @@ import (
type SegmentStyle string
func (s *SegmentStyle) resolve(context any) SegmentStyle {
value, err := template.Render(string(*s), context)
value, err := template.RenderTrusted(string(*s), context)
// default to Plain
if err != nil || value == "" {
@@ -301,7 +301,7 @@ func (segment *Segment) renderFallback(index int) bool {
return false
}
text, err := template.Render(segment.FallbackTemplate, segment.writer)
text, err := template.RenderTrusted(segment.FallbackTemplate, segment.writer)
if err != nil {
text = err.Error()
}
@@ -580,7 +580,7 @@ func (segment *Segment) string() string {
segment.Template = segment.writer.Template()
}
text, err := template.Render(segment.Template, segment.writer)
text, err := template.RenderTrusted(segment.Template, segment.writer)
if err != nil {
return err.Error()
}
+3 -3
View File
@@ -117,7 +117,7 @@ func (e *Engine) pwd() {
}
// Allow template logic to define when to enable the PWD (when supported)
pwdType, err := template.Render(e.Config.PWD, nil)
pwdType, err := template.RenderTrusted(e.Config.PWD, nil)
if err != nil || pwdType == "" {
return
}
@@ -181,7 +181,7 @@ func (e *Engine) shouldFill(filler string, padLength int) (string, bool) {
}()
var err error
if filler, err = template.Render(filler, e); err != nil {
if filler, err = template.RenderTrusted(filler, e); err != nil {
return "", false
}
@@ -202,7 +202,7 @@ func (e *Engine) shouldFill(filler string, padLength int) (string, bool) {
}
func (e *Engine) getTitleTemplateText() string {
if txt, err := template.Render(e.Config.ConsoleTitleTemplate, nil); err == nil {
if txt, err := template.RenderTrusted(e.Config.ConsoleTitleTemplate, nil); err == nil {
return txt
}
+2 -2
View File
@@ -57,7 +57,7 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
}
}
promptText, err := template.Render(getTemplate(prompt.Template), nil)
promptText, err := template.RenderTrusted(getTemplate(prompt.Template), nil)
if err != nil {
promptText = err.Error()
}
@@ -192,7 +192,7 @@ func (e *Engine) renderRightTemplate(prompt *config.Segment, background, foregro
return "", 0
}
text, err := template.Render(prompt.RightTemplate, nil)
text, err := template.RenderTrusted(prompt.RightTemplate, nil)
if err != nil {
text = err.Error()
}
+1 -1
View File
@@ -32,7 +32,7 @@ func (h *HTTP) Enabled() bool {
method := h.options.String(METHOD, "GET")
timeout := h.options.Int(options.HTTPTimeout, 10000)
if resolved, err := template.Render(url, nil); err == nil {
if resolved, err := template.RenderTrusted(url, nil); err == nil {
url = resolved
}
+1 -1
View File
@@ -358,7 +358,7 @@ func (l *Language) buildVersionURL() {
return
}
url, err := template.Render(versionURLTemplate, l.Version)
url, err := template.RenderTrusted(versionURLTemplate, l.Version)
if err != nil {
return
}
+1 -1
View File
@@ -105,7 +105,7 @@ func (m Map) Template(option Option, defaultValue string, context any) string {
return value
}
resolved, err := template.Render(value, context)
resolved, err := template.RenderTrusted(value, context)
if err != nil {
debugf("%s: template error, using raw value: %s", option, err)
return value
+9 -4
View File
@@ -282,7 +282,12 @@ func (pt *Path) setStyle() {
}
// make sure we resolve all templates
if txt, err := template.Render(pt.Path, pt); err == nil {
//
// pt.Path is composed from raw filesystem folder names (untrusted) plus
// already-rendered config templates, so it must never get the func map
// entries that touch the OS (cmd/readFile/stat/glob) — use the restricted
// renderer, not template.Render.
if txt, err := template.RenderUntrusted(pt.Path, pt); err == nil {
pt.Path = txt
}
}
@@ -293,7 +298,7 @@ func (pt *Path) getMaxWidth() int {
return 0
}
txt, err := template.Render(width, pt)
txt, err := template.RenderTrusted(width, pt)
if err != nil {
log.Error(err)
return 0
@@ -320,7 +325,7 @@ func (pt *Path) getFolderSeparator() string {
return separator
}
txt, err := template.Render(separatorTemplate, pt)
txt, err := template.RenderTrusted(separatorTemplate, pt)
if err != nil {
log.Error(err)
}
@@ -652,7 +657,7 @@ func (pt *Path) setMappedLocations() {
continue
}
location, err := template.Render(key, pt)
location, err := template.RenderTrusted(key, pt)
if err != nil {
log.Error(err)
}
+1 -1
View File
@@ -28,7 +28,7 @@ func renderTemplateNoTrimSpace(env *mock.Environment, segmentTemplate string, co
}
template.Init(env, nil, nil)
text, err := template.Render(segmentTemplate, context)
text, err := template.RenderTrusted(segmentTemplate, context)
if err != nil {
return err.Error()
}
+7
View File
@@ -630,6 +630,13 @@ var testFullAndFolderPathCases = []testFullAndFolderPathCase{
{Style: Full, Pwd: homeDir + abc, Expected: "~/abc"},
{Style: Full, Pwd: homeDir + abc, Expected: homeDir + abc, DisableMappedLocations: true},
{Style: Full, Pwd: abcd, Expected: abcd},
// A folder name containing template syntax must never execute the `cmd` function
// once it is spliced into pt.Path and re-rendered in setStyle(): the render uses
// the restricted func map, so parsing fails and pt.Path keeps its raw, unexecuted
// text instead.
{Style: Full, Pwd: homeDir + "/{{ cmd `whoami` }}", Expected: "~/{{ cmd `whoami` }}"},
{Style: FolderType, Pwd: homeDir + "/{{ cmd `whoami` }}", Expected: "{{ cmd `whoami` }}"},
}
var testFullPathCustomMappedLocationsCases = []testFullPathCustomMappedLocationsCase{
+10
View File
@@ -421,6 +421,16 @@ var testFullAndFolderPathCases = []testFullAndFolderPathCase{
{Style: Full, FolderSeparatorIcon: `\`, Pwd: homeDirWindows, Expected: "~", PathSeparator: `\`, GOOS: runtime.WINDOWS},
{Style: Full, FolderSeparatorIcon: `\`, Pwd: homeDirWindows + "\\abc", Expected: "~\\abc", PathSeparator: `\`, GOOS: runtime.WINDOWS},
{Style: Full, FolderSeparatorIcon: `\`, Pwd: "C:\\Users\\posh", Expected: "C:\\Users\\posh", PathSeparator: `\`, GOOS: runtime.WINDOWS},
// A folder name containing template syntax must never execute the `cmd` function
// once it is spliced into pt.Path and re-rendered in setStyle(): the render uses
// the restricted func map, so parsing fails and pt.Path keeps its raw, unexecuted
// text instead.
{
Style: FolderType, FolderSeparatorIcon: `\`,
Pwd: homeDirWindows + "\\{{ cmd `whoami` }}", Expected: "{{ cmd `whoami` }}",
PathSeparator: `\`, GOOS: runtime.WINDOWS,
},
}
var testFullPathCustomMappedLocationsCases = []testFullPathCustomMappedLocationsCase{
+1 -1
View File
@@ -163,7 +163,7 @@ func (s *Scm) formatBranch(branch string) string {
return branch
}
txt, err := template.Render(branchTemplate, struct{ Branch, Upstream string }{Branch: branch, Upstream: s.Upstream})
txt, err := template.RenderTrusted(branchTemplate, struct{ Branch, Upstream string }{Branch: branch, Upstream: s.Upstream})
if err != nil {
return branch
}
+2 -2
View File
@@ -48,7 +48,7 @@ func (s *Status) formatStatus(status int, pipeStatus string) string {
}
if pipeStatus == "" {
if txt, err := template.Render(statusTemplate, s); err == nil {
if txt, err := template.RenderTrusted(statusTemplate, s); err == nil {
return txt
}
@@ -87,7 +87,7 @@ func (s *Status) formatStatus(status int, pipeStatus string) string {
context.Code = code
txt, err := template.Render(statusTemplate, context)
txt, err := template.RenderTrusted(statusTemplate, context)
if err != nil {
write(codeStr)
continue
+3 -3
View File
@@ -19,7 +19,7 @@ func BenchmarkRenderPlain(b *testing.B) {
setupTemplateBench()
b.ReportAllocs()
for b.Loop() {
_, _ = Render("plain text without any template markers at all", nil)
_, _ = RenderTrusted("plain text without any template markers at all", nil)
}
}
@@ -33,7 +33,7 @@ func BenchmarkRenderSimple(b *testing.B) {
data := ctx{Shell: "pwsh", UserName: "jandedobbeleer"}
b.ReportAllocs()
for b.Loop() {
_, _ = Render("{{ .Shell }} {{ .UserName }}", data)
_, _ = RenderTrusted("{{ .Shell }} {{ .UserName }}", data)
}
}
@@ -56,7 +56,7 @@ func BenchmarkRenderRepeated(b *testing.B) {
tmpl := `{{ if .Root }}# {{ end }}{{ .Folder }}{{ if .Branch }} on {{ .Branch }}{{ end }} {{ if .Version }}v{{ .Version }}{{ end }}`
b.ReportAllocs()
for b.Loop() {
_, _ = Render(tmpl, data)
_, _ = RenderTrusted(tmpl, data)
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ func TestCmd(t *testing.T) {
Cache = new(cache.Template)
Init(e, nil, nil)
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err, tc.Case)
continue
+2 -2
View File
@@ -70,7 +70,7 @@ func TestDateFromStringEpoch(t *testing.T) {
Cache = new(cache.Template)
Init(env, nil, nil)
text, err := Render(tc.Template, tc.Context)
text, err := RenderTrusted(tc.Template, tc.Context)
assert.NoError(t, err, tc.Case)
if tc.Expected != "" {
assert.Equal(t, tc.Expected, text, tc.Case)
@@ -114,7 +114,7 @@ func TestDateAndHTMLDateFunctions(t *testing.T) {
Cache = new(cache.Template)
Init(env, nil, nil)
text, err := Render(tc.Template, tc.Context)
text, err := RenderTrusted(tc.Template, tc.Context)
assert.NoError(t, err, tc.Case)
assert.Equal(t, tc.Expected, text, tc.Case)
}
+1 -1
View File
@@ -28,7 +28,7 @@ func TestGlob(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err)
continue
+54 -9
View File
@@ -8,13 +8,25 @@ import (
"github.com/Masterminds/sprig/v3"
)
// sharedFuncMap is built exactly once and reused across all template constructions.
var sharedFuncMap = sync.OnceValue(func() template.FuncMap {
// dangerousFuncs execute host commands or touch the filesystem/environment
// directly. They are only exposed to templates rendered via RenderTrusted
// (see text.go) — never to RenderUntrusted, which may contain or be composed
// from runtime data.
var dangerousFuncs = map[string]bool{
"cmd": true,
"readFile": true,
"stat": true,
"glob": true,
"env": true,
"expandenv": true,
}
// baseFuncMap returns the funcs available regardless of trust level.
func baseFuncMap() map[string]any {
fm := map[string]any{
"secondsRound": secondsRound,
"url": url,
"path": filePath,
"glob": glob,
"matchP": matchP,
"findP": findP,
"replaceP": replaceP,
@@ -25,9 +37,6 @@ var sharedFuncMap = sync.OnceValue(func() template.FuncMap {
"hresult": hresult,
"trunc": trunc,
"truncE": TruncE,
"cmd": cmd,
"readFile": readFile,
"stat": stat,
"dir": filepath.Dir,
"base": filepath.Base,
// Locale-aware date/time formatting using OS regional settings.
@@ -42,15 +51,51 @@ var sharedFuncMap = sync.OnceValue(func() template.FuncMap {
}
for key, fun := range sprig.TxtFuncMap() {
if dangerousFuncs[key] {
continue
}
if _, ok := fm[key]; !ok {
fm[key] = fun
}
}
return fm
}
// sharedFuncMap is built exactly once and reused across all trusted template constructions.
var sharedFuncMap = sync.OnceValue(func() template.FuncMap {
fm := baseFuncMap()
fm["cmd"] = cmd
fm["readFile"] = readFile
fm["stat"] = stat
fm["glob"] = glob
sprigFuncs := sprig.TxtFuncMap()
if fn, ok := sprigFuncs["env"]; ok {
fm["env"] = fn
}
if fn, ok := sprigFuncs["expandenv"]; ok {
fm["expandenv"] = fn
}
return template.FuncMap(fm)
})
// funcMap returns the shared merged FuncMap (built once, reused everywhere).
func funcMap() template.FuncMap {
return sharedFuncMap()
// restrictedFuncMap is built exactly once and reused across all restricted template
// constructions (see RenderRestricted). It never contains dangerousFuncs.
var restrictedFuncMap = sync.OnceValue(func() template.FuncMap {
return template.FuncMap(baseFuncMap())
})
// funcMap returns the merged FuncMap for the given trust level (built once per
// level, reused everywhere).
func funcMap(trusted bool) template.FuncMap {
if trusted {
return sharedFuncMap()
}
return restrictedFuncMap()
}
+2 -2
View File
@@ -28,7 +28,7 @@ func TestUrl(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err)
continue
@@ -57,7 +57,7 @@ func TestPath(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
text, _ := Render(tc.Template, nil)
text, _ := RenderTrusted(tc.Template, nil)
assert.Equal(t, tc.Expected, text, tc.Case)
}
+2 -2
View File
@@ -42,7 +42,7 @@ func (l List) Join(context any) string {
buffer := text.NewBuilder()
for _, tmpl := range l {
value, err := Render(tmpl, context)
value, err := RenderTrusted(tmpl, context)
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
}
@@ -59,7 +59,7 @@ func (l List) FirstMatch(context any, defaultValue string) string {
}
for _, tmpl := range l {
value, err := Render(tmpl, context)
value, err := RenderTrusted(tmpl, context)
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
}
+5 -5
View File
@@ -120,7 +120,7 @@ func TestLocaleShortDateFallback(t *testing.T) {
tmpl := `{{ localeShortDate .T }}`
ctx := struct{ T time.Time }{T: time.Unix(0, 0).UTC()}
got, err := Render(tmpl, ctx)
got, err := RenderTrusted(tmpl, ctx)
assert.NoError(t, err)
assert.Equal(t, "1970-01-01", got)
}
@@ -142,7 +142,7 @@ func TestLocaleShortTimeFallback(t *testing.T) {
tmpl := `{{ localeShortTime .T }}`
ctx := struct{ T time.Time }{T: time.Unix(0, 0).UTC()}
got, err := Render(tmpl, ctx)
got, err := RenderTrusted(tmpl, ctx)
assert.NoError(t, err)
assert.Equal(t, "00:00", got)
}
@@ -165,7 +165,7 @@ func TestLocaleShortDateWithISOLayout(t *testing.T) {
// knownEpoch = 2019-06-13 20:39:39 UTC (from date_test.go)
tmpl := `{{ localeShortDate .T }}`
ctx := struct{ T time.Time }{T: time.Unix(knownEpoch, 0).UTC()}
got, err := Render(tmpl, ctx)
got, err := RenderTrusted(tmpl, ctx)
assert.NoError(t, err)
assert.Equal(t, "2019-06-13", got)
}
@@ -186,7 +186,7 @@ func TestLocaleShortTimeWith24hLayout(t *testing.T) {
tmpl := `{{ localeShortTime .T }}`
ctx := struct{ T time.Time }{T: time.Unix(knownEpoch, 0).UTC()}
got, err := Render(tmpl, ctx)
got, err := RenderTrusted(tmpl, ctx)
assert.NoError(t, err)
assert.Equal(t, "20:39", got)
}
@@ -207,7 +207,7 @@ func TestLocaleShortDateWith12hUSLayout(t *testing.T) {
tmpl := `{{ localeShortDate .T }} {{ localeShortTime .T }}`
ctx := struct{ T time.Time }{T: time.Unix(knownEpoch, 0).UTC()}
got, err := Render(tmpl, ctx)
got, err := RenderTrusted(tmpl, ctx)
assert.NoError(t, err)
assert.Equal(t, "6/13/2019 8:39 PM", got)
}
+1 -1
View File
@@ -18,7 +18,7 @@ func TestHResult(t *testing.T) {
}
for _, tc := range cases {
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err)
continue
+2 -2
View File
@@ -16,12 +16,12 @@ func TestTextPool(t *testing.T) {
Init(env, nil, nil)
// Test rendering
result, err := Render("Hello {{ .Name }}", map[string]any{"Name": "World"})
result, err := RenderTrusted("Hello {{ .Name }}", map[string]any{"Name": "World"})
assert.NoError(t, err)
assert.Equal(t, "Hello World", result)
// Test empty template
result2, err := Render("", nil)
result2, err := RenderTrusted("", nil)
assert.NoError(t, err)
assert.Equal(t, "", result2)
}
+22 -15
View File
@@ -5,6 +5,7 @@ import (
"errors"
"reflect"
"sort"
"strconv"
"strings"
"text/template"
"unicode"
@@ -47,14 +48,19 @@ func (t *renderer) release() {
}
// templateCacheKey returns the key used to look up a cached *template.Template.
// It encodes the raw (unpatched) template text and, when context is non-nil,
// the reflect.Type of the context so that two different struct types whose
// patchTemplate output would differ (via hasField) get separate cache entries.
// For map[string]any contexts the key also includes the sorted exported key
// names, since patchTemplate output depends on which keys are present.
func templateCacheKey(rawText string, ctx any) string {
// It encodes the raw (unpatched) template text, the trust level (parsed
// templates are bound to a func map at Parse time, so a trusted and a
// restricted render of identical text must never share a cache entry), and,
// when context is non-nil, the reflect.Type of the context so that two
// different struct types whose patchTemplate output would differ (via
// hasField) get separate cache entries. For map[string]any contexts the key
// also includes the sorted exported key names, since patchTemplate output
// depends on which keys are present.
func templateCacheKey(rawText string, trusted bool, ctx any) string {
key := rawText + "\x00" + strconv.FormatBool(trusted)
if ctx == nil {
return rawText
return key
}
t := reflect.TypeOf(ctx)
@@ -67,11 +73,11 @@ func templateCacheKey(rawText string, ctx any) string {
}
}
sort.Strings(keys)
return rawText + "\x00" + t.String() + "\x00" + strings.Join(keys, "\x01")
return key + "\x00" + t.String() + "\x00" + strings.Join(keys, "\x01")
}
}
return rawText + "\x00" + t.String()
return key + "\x00" + t.String()
}
// parsedTemplate returns a fully-parsed *template.Template for text.
@@ -79,10 +85,11 @@ func templateCacheKey(rawText string, ctx any) string {
// return the cached value. Concurrent first-renders of the same template
// may both parse, but LoadOrStore ensures only one result is shared.
func parsedTemplate(text *Text) (*template.Template, error) {
// Key on the raw, unpatched template text plus the context type so that
// cache hits can skip patchTemplate entirely: patching is only needed
// the first time a given (raw template, context type) pair is seen.
key := templateCacheKey(text.template, text.context)
// Key on the raw, unpatched template text plus the trust level and context
// type so that cache hits can skip patchTemplate entirely: patching is only
// needed the first time a given (raw template, trust level, context type)
// combination is seen.
key := templateCacheKey(text.template, text.trusted, text.context)
if cached, ok := parsedTemplates.Load(key); ok {
return cached.(*template.Template), nil
@@ -91,8 +98,8 @@ func parsedTemplate(text *Text) (*template.Template, error) {
// Cache miss: patch the raw template into its executable form.
text.patchTemplate()
// Parse into a fresh template with the shared func map and settings.
tmpl, err := template.New("cache").Funcs(funcMap()).Parse(text.template)
// Parse into a fresh template with the func map matching this render's trust level.
tmpl, err := template.New("cache").Funcs(funcMap(text.trusted)).Parse(text.template)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -25,7 +25,7 @@ func TestRoundSeconds(t *testing.T) {
}
for _, tc := range cases {
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err)
continue
+1 -1
View File
@@ -22,7 +22,7 @@ func TestTrunc(t *testing.T) {
}
for _, tc := range cases {
text, err := Render(tc.Template, nil)
text, err := RenderTrusted(tc.Template, nil)
if tc.ShouldError {
assert.Error(t, err)
continue
+27 -4
View File
@@ -13,24 +13,46 @@ import (
type Text struct {
context Data
template string
trusted bool
}
// New returns a Text instance from the pool with the given template and context
func get(template string, context any) *Text {
func get(template string, trusted bool, context any) *Text {
if textPool == nil {
// Fallback if pool is not initialized yet
return &Text{context: context, template: template}
return &Text{context: context, template: template, trusted: trusted}
}
text := textPool.Get()
text.template = template
text.trusted = trusted
text.context = context
return text
}
func Render(template string, context any) (string, error) {
t := get(template, context)
// RenderTrusted executes a template the caller has verified was authored by
// the user in their own configuration (a segment/block/palette/etc. template
// field), as opposed to text assembled at runtime from external data
// (filesystem names, command output, API responses, ...). The full func map
// is available, including cmd/readFile/stat/glob.
//
// Only call this with a string read verbatim from a config field — passing
// runtime-composed text here defeats the whole point of the split with
// RenderUntrusted.
func RenderTrusted(template string, context any) (string, error) {
return render(template, true, context)
}
// RenderUntrusted executes a template that may contain, or be composed from,
// runtime data rather than user-authored config text. cmd/readFile/stat/glob
// and env/expandenv are not available.
func RenderUntrusted(template string, context any) (string, error) {
return render(template, false, context)
}
func render(template string, trusted bool, context any) (string, error) {
t := get(template, trusted, context)
defer t.release()
if !strings.Contains(t.template, "{{") || !strings.Contains(t.template, "}}") {
@@ -47,6 +69,7 @@ func Render(template string, context any) (string, error) {
func (t *Text) release() {
t.context = nil
t.template = ""
t.trusted = false
if textPool != nil {
textPool.Put(t)
+3 -3
View File
@@ -162,7 +162,7 @@ func TestRenderTemplate(t *testing.T) {
Cache = new(cache.Template)
Init(env, nil, nil)
text, err := Render(tc.Template, tc.Context)
text, err := RenderTrusted(tc.Template, tc.Context)
if tc.ShouldError {
assert.Error(t, err)
continue
@@ -250,7 +250,7 @@ func TestRenderTemplateEnvVar(t *testing.T) {
}
Init(env, nil, nil)
text, err := Render(tc.Template, tc.Context)
text, err := RenderTrusted(tc.Template, tc.Context)
if tc.ShouldError {
assert.Error(t, err)
continue
@@ -400,7 +400,7 @@ func TestSegmentContains(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
text, _ := Render(tc.Template, nil)
text, _ := RenderTrusted(tc.Template, nil)
assert.Equal(t, tc.Expected, text, tc.Case)
}
}