Skip to content

Commit

Permalink
Add TrustedFS.
Browse files Browse the repository at this point in the history
Add the TrustedFS type, representing an fs.FS in a safe way.

The only ways to create a TrustedFS is from an embed.FS, or from a
TrustedSource. This ensures that a TrustedFS is always under
application control.

This new safehtml API requires Go 1.16 or higher, since
that is when fs.FS was introduced. That seems reasonable, since the Go
team supports only the last two versions, and we are now on 1.17.

PiperOrigin-RevId: 405723011
Change-Id: I3f8554b25b74630386956ab240a4d0a70db0aee2
  • Loading branch information
Safe HTML Team authored and Copybara-Service committed Oct 26, 2021
1 parent 2057dd9 commit d6f0e11
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 8 deletions.
24 changes: 16 additions & 8 deletions template/template.go
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
85 changes: 85 additions & 0 deletions 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
}
}
26 changes: 26 additions & 0 deletions 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")
}
}

0 comments on commit d6f0e11

Please sign in to comment.