diff --git a/template/template.go b/template/template.go index c52e3c8..efd0ef6 100644 --- a/template/template.go +++ b/template/template.go @@ -465,7 +465,7 @@ func stringConstantsToStrings(strs []stringConstant) []string { // an attacker, filenames must be untyped string constants, which are always under // programmer control. func ParseFiles(filenames ...stringConstant) (*Template, error) { - return parseFiles(nil, stringConstantsToStrings(filenames)...) + return parseFiles(nil, readFileOS, stringConstantsToStrings(filenames)...) } // ParseFilesFromTrustedSources creates a new Template and parses the template definitions from @@ -482,7 +482,7 @@ func ParseFiles(filenames ...stringConstant) (*Template, error) { // an attacker, filenames must be trusted sources, which are always under programmer // or application control. func ParseFilesFromTrustedSources(filenames ...TrustedSource) (*Template, error) { - return parseFiles(nil, trustedSourcesToStrings(filenames)...) + return parseFiles(nil, readFileOS, trustedSourcesToStrings(filenames)...) } // ParseFiles parses the named files and associates the resulting templates with @@ -498,7 +498,7 @@ func ParseFilesFromTrustedSources(filenames ...TrustedSource) (*Template, error) // an attacker, filenames must be untyped string constants, which are always under // programmer control. func (t *Template) ParseFiles(filenames ...stringConstant) (*Template, error) { - return parseFiles(t, stringConstantsToStrings(filenames)...) + return parseFiles(t, readFileOS, stringConstantsToStrings(filenames)...) } // ParseFilesFromTrustedSources parses the named files and associates the resulting templates with @@ -514,12 +514,13 @@ func (t *Template) ParseFiles(filenames ...stringConstant) (*Template, error) { // an attacker, filenames must be trusted sources, which are always under programmer // or application control. func (t *Template) ParseFilesFromTrustedSources(filenames ...TrustedSource) (*Template, error) { - return parseFiles(t, trustedSourcesToStrings(filenames)...) + return parseFiles(t, readFileOS, trustedSourcesToStrings(filenames)...) } // parseFiles is the helper for the method and function. If the argument // template is nil, it is created from the first file. -func parseFiles(t *Template, filenames ...string) (*Template, error) { +// readFile takes a filename and returns the file's basename and contents. +func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) { if err := t.checkCanParse(); err != nil { return nil, err } @@ -529,12 +530,11 @@ func parseFiles(t *Template, filenames ...string) (*Template, error) { return nil, fmt.Errorf("html/template: no files named in call to ParseFiles") } for _, filename := range filenames { - b, err := ioutil.ReadFile(filename) + name, b, err := readFile(filename) if err != nil { return nil, err } s := stringConstant(b) - name := filepath.Base(filename) // First template becomes return value if not already defined, // and we use that one for subsequent New calls to associate // all the templates together. Also, if this file has the same name @@ -558,6 +558,14 @@ func parseFiles(t *Template, filenames ...string) (*Template, error) { return t, nil } +// Copied with minor changes from +// https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go. +func readFileOS(file string) (string, []byte, error) { + name := filepath.Base(file) + b, err := ioutil.ReadFile(file) + return name, b, err +} + // ParseGlob creates a new Template and parses the template definitions from the // files identified by the pattern, which must match at least one file. The // returned template will have the (base) name and (parsed) contents of the @@ -632,7 +640,7 @@ func parseGlob(t *Template, pattern string) (*Template, error) { if len(filenames) == 0 { return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern) } - return parseFiles(t, filenames...) + return parseFiles(t, readFileOS, filenames...) } // IsTrue reports whether the value is 'true', in the sense of not the zero of its type, diff --git a/template/trustedfs.go b/template/trustedfs.go new file mode 100644 index 0000000..1a09e7a --- /dev/null +++ b/template/trustedfs.go @@ -0,0 +1,85 @@ +// Copyright (c) 2021 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +//go:build go1.16 +// +build go1.16 + +package template + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path" +) + +// A TrustedFS is an immutable type referencing a filesystem (fs.FS) +// under application control. +// +// In order to ensure that an attacker cannot influence the TrustedFS value, a +// TrustedFS can be instantiated in only two ways. One way is from an embed.FS +// with TrustedFSFromEmbed. It is assumed that embedded filesystems are under +// the programmer's control. The other way is from a TrustedSource using +// TrustedFSFromTrustedSource, in which case the guarantees and caveats of +// TrustedSource apply. +type TrustedFS struct { + fsys fs.FS +} + +// TrustedFSFromEmbed constructs a TrustedFS from an embed.FS. +func TrustedFSFromEmbed(fsys embed.FS) TrustedFS { + return TrustedFS{fsys: fsys} +} + +// TrustedFSFromTrustedSource constructs a TrustedFS from the string in the +// TrustedSource, which should refer to a directory. +func TrustedFSFromTrustedSource(ts TrustedSource) TrustedFS { + return TrustedFS{fsys: os.DirFS(ts.src)} +} + +// ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS +// instead of the host operating system's file system. +// It accepts a list of glob patterns. +// (Note that most file names serve as glob patterns matching only themselves.) +func ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) { + return parseFS(nil, tfs.fsys, patterns) +} + +// ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS +// instead of the host operating system's file system. +// It accepts a list of glob patterns. +// (Note that most file names serve as glob patterns matching only themselves.) +func (t *Template) ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) { + return parseFS(t, tfs.fsys, patterns) +} + +// Copied from +// https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go. +func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) { + var filenames []string + for _, pattern := range patterns { + list, err := fs.Glob(fsys, pattern) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + filenames = append(filenames, list...) + } + return parseFiles(t, readFileFS(fsys), filenames...) +} + +// Copied with minor changes from +// https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go. +func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { + return func(file string) (string, []byte, error) { + name := path.Base(file) + b, err := fs.ReadFile(fsys, file) + return name, b, err + } +} diff --git a/template/trustedfs_test.go b/template/trustedfs_test.go new file mode 100644 index 0000000..f89b46e --- /dev/null +++ b/template/trustedfs_test.go @@ -0,0 +1,26 @@ +// Copyright (c) 2021 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +//go:build go1.16 +// +build go1.16 + +package template + +import ( + "embed" + "testing" +) + +//go:embed testdata +var testFS embed.FS + +func TestParseFS(t *testing.T) { + tmpl := New("root") + parsedTmpl := Must(tmpl.ParseFS(TrustedFSFromEmbed(testFS), "testdata/glob_*.tmpl")) + if parsedTmpl != tmpl { + t.Errorf("expected ParseEmbedFS to update template") + } +}