"
50 | html := strings.Repeat(h1, 100) + h2 + strings.Repeat(h1, 100) + h2
51 |
52 | var buf bytes.Buffer
53 | for i := 0; i < b.N; i++ {
54 | tmpl := Must(New("foo").Parse(stringConstant(html)))
55 | if err := tmpl.Execute(&buf, r); err != nil {
56 | b.Fatal(err)
57 | }
58 | buf.Reset()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/template/trustedfs.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | //go:build go1.16
8 | // +build go1.16
9 |
10 | package template
11 |
12 | import (
13 | "embed"
14 | "fmt"
15 | "io/fs"
16 | "os"
17 | "path"
18 | )
19 |
20 | // A TrustedFS is an immutable type referencing a filesystem (fs.FS)
21 | // under application control.
22 | //
23 | // In order to ensure that an attacker cannot influence the TrustedFS value, a
24 | // TrustedFS can be instantiated in only two ways. One way is from an embed.FS
25 | // with TrustedFSFromEmbed. It is assumed that embedded filesystems are under
26 | // the programmer's control. The other way is from a TrustedSource using
27 | // TrustedFSFromTrustedSource, in which case the guarantees and caveats of
28 | // TrustedSource apply.
29 | type TrustedFS struct {
30 | fsys fs.FS
31 | }
32 |
33 | // TrustedFSFromEmbed constructs a TrustedFS from an embed.FS.
34 | func TrustedFSFromEmbed(fsys embed.FS) TrustedFS {
35 | return TrustedFS{fsys: fsys}
36 | }
37 |
38 | // TrustedFSFromTrustedSource constructs a TrustedFS from the string in the
39 | // TrustedSource, which should refer to a directory.
40 | func TrustedFSFromTrustedSource(ts TrustedSource) TrustedFS {
41 | return TrustedFS{fsys: os.DirFS(ts.src)}
42 | }
43 |
44 | // Sub returns a TrustedFS at a subdirectory of the receiver.
45 | // It works by calling fs.Sub on the receiver's fs.FS.
46 | func (tf TrustedFS) Sub(dir TrustedSource) (TrustedFS, error) {
47 | subfs, err := fs.Sub(tf.fsys, dir.String())
48 | return TrustedFS{fsys: subfs}, err
49 | }
50 |
51 | // ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS
52 | // instead of the host operating system's file system.
53 | // It accepts a list of glob patterns.
54 | // (Note that most file names serve as glob patterns matching only themselves.)
55 | //
56 | // The same behaviors listed for ParseFiles() apply to ParseFS too (e.g. using the base name
57 | // of the file as the template name).
58 | func ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) {
59 | return parseFS(nil, tfs.fsys, patterns)
60 | }
61 |
62 | // ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS
63 | // instead of the host operating system's file system.
64 | // It accepts a list of glob patterns.
65 | // (Note that most file names serve as glob patterns matching only themselves.)
66 | //
67 | // The same behaviors listed for ParseFiles() apply to ParseFS too (e.g. using the base name
68 | // of the file as the template name).
69 | func (t *Template) ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) {
70 | return parseFS(t, tfs.fsys, patterns)
71 | }
72 |
73 | // Copied from
74 | // https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go.
75 | func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) {
76 | var filenames []string
77 | for _, pattern := range patterns {
78 | list, err := fs.Glob(fsys, pattern)
79 | if err != nil {
80 | return nil, err
81 | }
82 | if len(list) == 0 {
83 | return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
84 | }
85 | filenames = append(filenames, list...)
86 | }
87 | return parseFiles(t, readFileFS(fsys), filenames...)
88 | }
89 |
90 | // Copied with minor changes from
91 | // https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go.
92 | func readFileFS(fsys fs.FS) func(string) (string, []byte, error) {
93 | return func(file string) (string, []byte, error) {
94 | name := path.Base(file)
95 | b, err := fs.ReadFile(fsys, file)
96 | return name, b, err
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/template/trustedfs_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | //go:build go1.16
8 | // +build go1.16
9 |
10 | package template
11 |
12 | import (
13 | "embed"
14 | "testing"
15 | )
16 |
17 | //go:embed testdata
18 | var testFS embed.FS
19 |
20 | func TestParseFS(t *testing.T) {
21 | tmpl := New("root")
22 | parsedTmpl := Must(tmpl.ParseFS(TrustedFSFromEmbed(testFS), "testdata/glob_*.tmpl"))
23 | if parsedTmpl != tmpl {
24 | t.Errorf("expected ParseFS to update template")
25 | }
26 | }
27 |
28 | func TestSub(t *testing.T) {
29 | tfs := TrustedFSFromEmbed(testFS)
30 | sub, err := tfs.Sub(TrustedSourceFromConstant("testdata"))
31 | if err != nil {
32 | t.Fatal(err)
33 | }
34 | tmpl := New("t1")
35 | parsedTmpl := Must(tmpl.ParseFS(sub, "dir1/parsefiles_t1.tmpl"))
36 | if parsedTmpl != tmpl {
37 | t.Errorf("expected ParseFS to update template")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/template/trustedsource.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | package template
8 |
9 | import (
10 | "fmt"
11 | "os"
12 | "path/filepath"
13 | "strings"
14 |
15 | "flag"
16 | )
17 |
18 | // A TrustedSource is an immutable string-like type referencing
19 | // trusted template files under application control. It can be passed to
20 | // template-parsing functions and methods to safely load templates
21 | // without the risk of untrusted template execution.
22 | //
23 | // In order to ensure that an attacker cannot influence the TrustedSource
24 | // value, a TrustedSource can be instantiated only from untyped string
25 | // constants, command-line flags, and other application-controlled strings, but
26 | // never from arbitrary string values potentially representing untrusted user input.
27 | //
28 | // Note that TrustedSource's constructors cannot truly guarantee that the
29 | // templates it references are not attacker-controlled; it can guarantee only that
30 | // the path to the template itself is under application control. Users of these
31 | // constructors must ensure themselves that TrustedSource never references
32 | // attacker-controlled files or directories that contain such files.
33 | type TrustedSource struct {
34 | // We declare a TrustedSource not as a string but as a struct wrapping a string
35 | // to prevent construction of TrustedSource values through string conversion.
36 | src string
37 | }
38 |
39 | // TrustedSourceFromConstant constructs a TrustedSource with its underlying
40 | // src set to the given src, which must be an untyped string constant.
41 | //
42 | // No runtime validation or sanitization is performed on src; being under
43 | // application control, it is simply assumed to comply with the TrustedSource type
44 | // contract.
45 | func TrustedSourceFromConstant(src stringConstant) TrustedSource {
46 | return TrustedSource{string(src)}
47 | }
48 |
49 | // TrustedSourceFromConstantDir constructs a TrustedSource calling path/filepath.Join on
50 | // an application-controlled directory path, which must be an untyped string constant,
51 | // a TrustedSource, and a dynamic filename. It returns an error if filename contains
52 | // filepath or list separators, since this might cause the resulting path to reference a
53 | // file outside of the given directory.
54 | //
55 | // dir or src may be empty if either of these path segments are not required.
56 | func TrustedSourceFromConstantDir(dir stringConstant, src TrustedSource, filename string) (TrustedSource, error) {
57 | if i := strings.IndexAny(filename, string([]rune{filepath.Separator, filepath.ListSeparator})); i != -1 {
58 | return TrustedSource{}, fmt.Errorf("filename %q must not contain the separator %q", filename, filename[i])
59 | }
60 | if filename == ".." {
61 | return TrustedSource{}, fmt.Errorf("filename must not be the special name %q", filename)
62 | }
63 | return TrustedSource{filepath.Join(string(dir), src.String(), filename)}, nil
64 | }
65 |
66 | // TrustedSourceJoin is a wrapper around path/filepath.Join that returns a
67 | // TrustedSource formed by joining the given path elements into a single path,
68 | // adding an OS-specific path separator if necessary.
69 | func TrustedSourceJoin(elem ...TrustedSource) TrustedSource {
70 | return TrustedSource{filepath.Join(trustedSourcesToStrings(elem)...)}
71 | }
72 |
73 | // TrustedSourceFromFlag returns a TrustedSource containing the string
74 | // representation of the retrieved value of the flag.
75 | //
76 | // In a server setting, flags are part of the application's deployment
77 | // configuration and are hence considered application-controlled.
78 | func TrustedSourceFromFlag(value flag.Value) TrustedSource {
79 | return TrustedSource{fmt.Sprint(value.String())}
80 | }
81 |
82 | // TrustedSourceFromEnvVar is a wrapper around os.Getenv that
83 | // returns a TrustedSource containing the value of the environment variable
84 | // named by the key. It returns the value, which will be empty if the variable
85 | // is not present. To distinguish between an empty value and an unset value,
86 | // use os.LookupEnv.
87 | //
88 | // In a server setting, environment variables are part of the application's
89 | // deployment configuration and are hence considered application-controlled.
90 | func TrustedSourceFromEnvVar(key stringConstant) TrustedSource {
91 | return TrustedSource{os.Getenv(string(key))}
92 | }
93 |
94 | // String returns the string form of the TrustedSource.
95 | func (t TrustedSource) String() string {
96 | return t.src
97 | }
98 |
99 | func trustedSourcesToStrings(paths []TrustedSource) []string {
100 | ret := make([]string, 0, len(paths))
101 | for _, p := range paths {
102 | ret = append(ret, p.String())
103 | }
104 | return ret
105 | }
106 |
--------------------------------------------------------------------------------
/template/trustedsource_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | package template
8 |
9 | import (
10 | "fmt"
11 | "os"
12 | "testing"
13 | )
14 |
15 | func TestTrustedSourceFromConstant(t *testing.T) {
16 | const want = `foo`
17 | if got := TrustedSourceFromConstant(want).String(); got != want {
18 | t.Errorf("got: %q, want: %q", got, want)
19 | }
20 | }
21 |
22 | func TestTrustedSourceFromConstantDir(t *testing.T) {
23 | // Use a short alias to make test cases more readable.
24 | c := TrustedSourceFromConstant
25 | for _, test := range [...]struct {
26 | dir string
27 | src TrustedSource
28 | filename, want, err string
29 | }{
30 | {"foo/", c(""), "file", "foo/file", ""},
31 | {"foo/", TrustedSource{}, "file", "foo/file", ""},
32 | {"", c("foo/"), "file", "foo/file", ""},
33 | {"foo", c("bar"), "file", "foo/bar/file", ""},
34 | {"foo/bar", c("baz"), "file", "foo/bar/baz/file", ""},
35 | {"foo/bar", c("baz"), "file.html", "foo/bar/baz/file.html", ""},
36 | {"foo", c("bar"), "dir:otherPath", "", `filename "dir:otherPath" must not contain the separator ':'`},
37 | {"foo", c("bar"), "dir/file.html", "", `filename "dir/file.html" must not contain the separator '/'`},
38 | {"foo", c("bar"), "../file.html", "", `filename "../file.html" must not contain the separator '/'`},
39 | {"foo/bar", c("baz"), "..", "", `filename must not be the special name ".."`},
40 | } {
41 | ts, err := TrustedSourceFromConstantDir(stringConstant(test.dir), test.src, test.filename)
42 | prefix := fmt.Sprintf("dir %q src %q filename %q", test.dir, test.src, test.filename)
43 | if test.err == "" && err != nil {
44 | t.Errorf("%s : unexpected error: %s", prefix, err)
45 | } else if test.err != "" && err == nil {
46 | t.Errorf("%s : expected error", prefix)
47 | } else if test.err != "" && err.Error() != test.err {
48 | t.Errorf("%s : got error:\n\t%s\nwant:\n\t%s", prefix, err, test.err)
49 | } else if ts.String() != test.want {
50 | t.Errorf("%s : got %q, want %q", prefix, ts.String(), test.want)
51 | }
52 | }
53 | }
54 |
55 | func TestTrustedSourceJoin(t *testing.T) {
56 | // Use a short alias to make test cases more readable.
57 | c := TrustedSourceFromConstant
58 | for _, test := range [...]struct {
59 | desc string
60 | in []TrustedSource
61 | want string
62 | }{
63 | {"Path separators added if necessary",
64 | []TrustedSource{c("foo"), c("bar/"), c("/baz"), c("/far")},
65 | "foo/bar/baz/far",
66 | },
67 |
68 | {".. path segments formed by concatenating multiple TrustedSource values do not affect the resultant path",
69 | []TrustedSource{c("foo"), c("bar/."), c("./baz")},
70 | "foo/bar/baz",
71 | },
72 | {
73 | ".. path segments that are explicitly specified in individual TrustedSource values will take effect",
74 | []TrustedSource{c("foo"), c("bar"), c("baz/.."), c("../far")},
75 | "foo/far",
76 | },
77 | } {
78 | if got := TrustedSourceJoin(test.in...).String(); got != test.want {
79 | t.Errorf("%s : got: %q, want: %q", test.desc, got, test.want)
80 | }
81 | }
82 | }
83 |
84 | type testFlagValue string
85 |
86 | func (t *testFlagValue) String() string { return string(*t) }
87 |
88 | func (t *testFlagValue) Get() interface{} { return *t }
89 |
90 | func (t *testFlagValue) Set(s string) error {
91 | *t = testFlagValue(s)
92 | return nil
93 | }
94 |
95 | func TestTrustedSourceFromFlag(t *testing.T) {
96 | const want = `foo`
97 | value := testFlagValue(want)
98 | if got := TrustedSourceFromFlag(&value).String(); got != want {
99 | t.Errorf("got: %q, want: %q", got, want)
100 | }
101 | }
102 |
103 | func TestTrustedSourceFromEnvVar(t *testing.T) {
104 | const tmpDirEnvVar = `TMPDIR`
105 | const want = `/my/tmp`
106 | os.Setenv(tmpDirEnvVar, want)
107 | defer os.Unsetenv(tmpDirEnvVar)
108 | if got := TrustedSourceFromEnvVar(tmpDirEnvVar).String(); got != want {
109 | t.Errorf("got: %q, want: %q", got, want)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/template/trustedtemplate.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | package template
8 |
9 | // A TrustedTemplate is an immutable string-like type containing a
10 | // safehtml/template template body. It can be safely loaded as template
11 | // text without the risk of untrusted template execution.
12 | //
13 | // In order to ensure that an attacker cannot influence the TrustedTemplate
14 | // value, a TrustedTemplate can be instantiated only from untyped string constants,
15 | // and never from arbitrary string values potentially representing untrusted user input.
16 | type TrustedTemplate struct {
17 | // We declare a TrustedTemplate not as a string but as a struct wrapping a string
18 | // to prevent construction of TrustedTemplate values through string conversion.
19 | tmpl string
20 | }
21 |
22 | // MakeTrustedTemplate constructs a TrustedTemplate with its underlying
23 | // tmpl set to the given tmpl, which must be an untyped string constant.
24 | //
25 | // No runtime validation or sanitization is performed on tmpl; being under
26 | // application control, it is simply assumed to comply with the TrustedTemplate type
27 | // contract.
28 | func MakeTrustedTemplate(tmpl stringConstant) TrustedTemplate {
29 | return TrustedTemplate{string(tmpl)}
30 | }
31 |
32 | // String returns the string form of the TrustedTemplate.
33 | func (t TrustedTemplate) String() string {
34 | return t.tmpl
35 | }
36 |
--------------------------------------------------------------------------------
/template/trustedtemplate_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | package template
8 |
9 | import (
10 | "testing"
11 | )
12 |
13 | func TestMakeTrustedTemplate(t *testing.T) {
14 | const want = `foo`
15 | if got := MakeTrustedTemplate(want).String(); got != want {
16 | t.Errorf("got: %q, want: %q", got, want)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/template/uncheckedconversions/uncheckedconversions.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | // Package uncheckedconversions provides functions to create values of
8 | // safehtml/template types from plain strings. Use of these
9 | // functions could potentially result in safehtml/template type values that
10 | // violate their type contract, and hence result in security vulnerabilties.
11 | package uncheckedconversions
12 |
13 | import (
14 | "github.com/google/safehtml/internal/template/raw"
15 | "github.com/google/safehtml/template"
16 | )
17 |
18 | var trustedSource = raw.TrustedSource.(func(string) template.TrustedSource)
19 | var trustedTemplate = raw.TrustedTemplate.(func(string) template.TrustedTemplate)
20 |
21 | // TrustedSourceFromStringKnownToSatisfyTypeContract converts a string into a TrustedSource.
22 | func TrustedSourceFromStringKnownToSatisfyTypeContract(s string) template.TrustedSource {
23 | return trustedSource(s)
24 | }
25 |
26 | // TrustedTemplateFromStringKnownToSatisfyTypeContract converts a string into a TrustedTemplate.
27 | func TrustedTemplateFromStringKnownToSatisfyTypeContract(s string) template.TrustedTemplate {
28 | return trustedTemplate(s)
29 | }
30 |
--------------------------------------------------------------------------------
/template/uncheckedconversions/uncheckedconversions_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | package uncheckedconversions
8 |
9 | import (
10 | "testing"
11 | )
12 |
13 | func TestTrustedSourceFromStringKnownToSatisfyTypeContract(t *testing.T) {
14 | src := `some src`
15 | if out := TrustedSourceFromStringKnownToSatisfyTypeContract(src).String(); src != out {
16 | t.Errorf("uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q",
17 | src, out, src)
18 | }
19 | }
20 |
21 | func TestTrustedTemplateFromStringKnownToSatisfyTypeContract(t *testing.T) {
22 | tmpl := `some tmpl`
23 | if out := TrustedTemplateFromStringKnownToSatisfyTypeContract(tmpl).String(); tmpl != out {
24 | t.Errorf("uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q",
25 | tmpl, out, tmpl)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/template/url.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package template
6 |
7 | import (
8 | "fmt"
9 | "html"
10 | "regexp"
11 | "strings"
12 |
13 | "github.com/google/safehtml/internal/safehtmlutil"
14 | "github.com/google/safehtml"
15 | )
16 |
17 | // urlPrefixValidators maps URL and TrustedResourceURL sanitization contexts to functions return an error
18 | // if the given string is unsafe to use as a URL prefix in that sanitization context.
19 | var urlPrefixValidators = map[sanitizationContext]func(string) error{
20 | sanitizationContextURL: validateURLPrefix,
21 | sanitizationContextTrustedResourceURLOrURL: validateURLPrefix,
22 | sanitizationContextTrustedResourceURL: validateTrustedResourceURLPrefix,
23 | }
24 |
25 | // startsWithFullySpecifiedSchemePattern matches strings that have a fully-specified scheme component.
26 | // See RFC 3986 Section 3.
27 | var startsWithFullySpecifiedSchemePattern = regexp.MustCompile(
28 | `^[[:alpha:]](?:[[:alnum:]]|[+.-])*:`)
29 |
30 | // validateURLPrefix validates if the given non-empty prefix is a safe safehtml.URL prefix.
31 | //
32 | // Prefixes are considered unsafe if they end in an incomplete HTML character reference
33 | // or percent-encoding character triplet.
34 | //
35 | // If the prefix contains a fully-specified scheme component, it is considered safe only if
36 | // it starts with an allowed scheme. See safehtml.URLSanitized for more details.
37 | //
38 | // Otherwise, the prefix is safe only if it contains '/', '?', or '#', since the presence of any
39 | // of these runes ensures that this prefix, when combined with some arbitrary suffix, cannot be
40 | // interpreted as a part of a scheme.
41 | func validateURLPrefix(prefix string) error {
42 | decoded, err := decodeURLPrefix(prefix)
43 | if err != nil {
44 | return err
45 | }
46 | switch {
47 | case startsWithFullySpecifiedSchemePattern.MatchString(decoded):
48 | if safehtml.URLSanitized(decoded).String() != decoded {
49 | return fmt.Errorf("URL prefix %q contains an unsafe scheme", prefix)
50 | }
51 | case !strings.ContainsAny(decoded, "/?#"):
52 | // If the URL prefix does not already have a ':' scheme delimiter, and does not contain
53 | // '/', '?', or '#', any ':' following this prefix will be intepreted as a scheme
54 | // delimiter, causing this URL prefix to be interpreted as being part of a scheme.
55 | // e.g. `