diff --git a/README.md b/README.md index 5d7e16db4..1c0819b9b 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,11 @@ template { # includes one of these functions, it will exit with an error. function_blacklist = [] + # If a sandbox path is provided, any path provided to the `file` function is + # prefixed by the sandbox path. Relative paths that try to traverse outside + # that prefix will exit with an error. + sandbox_path = "" + # This is the `minimum(:maximum)` to wait before rendering a new template to # disk and triggering a command, separated by a colon (`:`). If the optional # maximum value is omitted, it is assumed to be 4x the required minimum value. diff --git a/config/template.go b/config/template.go index 5fad0b299..4f69bfb60 100644 --- a/config/template.go +++ b/config/template.go @@ -79,6 +79,11 @@ type TemplateConfig struct { // FunctionBlacklist is a list of functions that this template is not // permitted to run. FunctionBlacklist []string `mapstructure:"function_blacklist"` + + // SandboxPath adds a prefix to any path provided to the `file` function + // and causes an error if a relative path tries to traverse outside that + // prefix. + SandboxPath *string `mapstructure:"sandbox_path"` } // DefaultTemplateConfig returns a configuration that is populated with the @@ -130,6 +135,7 @@ func (c *TemplateConfig) Copy() *TemplateConfig { for _, fun := range c.FunctionBlacklist { o.FunctionBlacklist = append(o.FunctionBlacklist, fun) } + o.SandboxPath = c.SandboxPath return &o } @@ -207,6 +213,9 @@ func (c *TemplateConfig) Merge(o *TemplateConfig) *TemplateConfig { for _, fun := range o.FunctionBlacklist { r.FunctionBlacklist = append(r.FunctionBlacklist, fun) } + if o.SandboxPath != nil { + r.SandboxPath = o.SandboxPath + } return r } @@ -275,6 +284,10 @@ func (c *TemplateConfig) Finalize() { if c.RightDelim == nil { c.RightDelim = String("") } + + if c.SandboxPath == nil { + c.SandboxPath = String("") + } } // GoString defines the printable version of this struct. @@ -298,6 +311,7 @@ func (c *TemplateConfig) GoString() string { "LeftDelim:%s, "+ "RightDelim:%s"+ "FunctionBlacklist:%s"+ + "SandboxPath:%s"+ "}", BoolGoString(c.Backup), StringGoString(c.Command), @@ -313,6 +327,7 @@ func (c *TemplateConfig) GoString() string { StringGoString(c.LeftDelim), StringGoString(c.RightDelim), c.FunctionBlacklist, + StringGoString(c.SandboxPath), ) } diff --git a/config/template_test.go b/config/template_test.go index 89c79639b..7d5233f76 100644 --- a/config/template_test.go +++ b/config/template_test.go @@ -449,8 +449,9 @@ func TestTemplateConfig_Finalize(t *testing.T) { Max: TimeDuration(0 * time.Second), Min: TimeDuration(0 * time.Second), }, - LeftDelim: String(""), - RightDelim: String(""), + LeftDelim: String(""), + RightDelim: String(""), + SandboxPath: String(""), }, }, } diff --git a/manager/runner.go b/manager/runner.go index 773a6dd6b..496cb05e0 100644 --- a/manager/runner.go +++ b/manager/runner.go @@ -16,8 +16,8 @@ import ( "github.com/hashicorp/consul-template/renderer" "github.com/hashicorp/consul-template/template" "github.com/hashicorp/consul-template/watch" - "github.com/hashicorp/go-multierror" - "github.com/mattn/go-shellwords" + multierror "github.com/hashicorp/go-multierror" + shellwords "github.com/mattn/go-shellwords" "github.com/pkg/errors" ) @@ -857,6 +857,7 @@ func (r *Runner) init() error { LeftDelim: config.StringVal(ctmpl.LeftDelim), RightDelim: config.StringVal(ctmpl.RightDelim), FunctionBlacklist: ctmpl.FunctionBlacklist, + SandboxPath: config.StringVal(ctmpl.SandboxPath), }) if err != nil { return err diff --git a/template/funcs.go b/template/funcs.go index caf03b1b3..63f316002 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "reflect" "regexp" "strconv" @@ -98,12 +99,15 @@ func executeTemplateFunc(t *template.Template) func(string, ...interface{}) (str } // fileFunc returns or accumulates file dependencies. -func fileFunc(b *Brain, used, missing *dep.Set) func(string) (string, error) { +func fileFunc(b *Brain, used, missing *dep.Set, sandboxPath string) func(string) (string, error) { return func(s string) (string, error) { if len(s) == 0 { return "", nil } - + s, err := sandboxedPath(sandboxPath, s) + if err != nil { + return "", err + } d, err := dep.NewFileQuery(s) if err != nil { return "", err @@ -1161,3 +1165,27 @@ func modulo(b, a interface{}) (interface{}, error) { func blacklisted(...string) (string, error) { return "", errors.New("function is disabled") } + +// sandboxedPath returns a path that's been prefixed by the sandbox path, +// if any. If a sandbox path was provided, it will return an error if the +// path falls outside the sandbox. +func sandboxedPath(sandbox, s string) (string, error) { + path, err := filepath.Abs(filepath.Join(sandbox, s)) + if err != nil { + return "", err + } + if sandbox != "" { + path, err := filepath.EvalSymlinks(path) + if err != nil { + return "", err + } + path, err = filepath.Rel(sandbox, path) + if err != nil { + return "", err + } + if strings.HasPrefix(path, "..") { + return "", fmt.Errorf("'%s' is outside of sandbox", s) + } + } + return path, nil +} diff --git a/template/funcs_test.go b/template/funcs_test.go new file mode 100644 index 000000000..88ffd5792 --- /dev/null +++ b/template/funcs_test.go @@ -0,0 +1,93 @@ +package template + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/pkg/errors" +) + +func TestFileSandbox(t *testing.T) { + // while most of the function can be tested lexigraphically, + // we need to be able to walk actual symlinks. + _, filename, _, _ := runtime.Caller(0) + sandboxDir := filepath.Join(filepath.Dir(filename), "testdata", "sandbox") + cwd, _ := os.Getwd() + cases := []struct { + name string + sandbox string + path string + expectedPath string + expectedErr error + }{ + { + "absolute_path_no_sandbox", + "", + "/path/to/file", + "/path/to/file", + nil, + }, + { + "relative_path_no_sandbox", + "", + "./path/to/file", + filepath.Join(cwd, "path/to/file"), + nil, + }, + { + "absolute_path_with_sandbox", + sandboxDir, + "/path/to/file", + filepath.Join(sandboxDir, "path/to/file"), + nil, + }, + { + "relative_path_in_sandbox", + sandboxDir, + "./path/to/file", + filepath.Join(sandboxDir, "path/to/file"), + nil, + }, + { + "symlink_path_in_sandbox", + sandboxDir, + "./path/to/ok-symlink", + filepath.Join(sandboxDir, "path/to/ok-symlink"), + nil, + }, + { + "relative_path_escaping_sandbox", + sandboxDir, + "/path/../../../funcs_test.go", + "", + errors.New("'/path/../../../funcs_test.go' is outside of sandbox"), + }, + { + "symlink_escaping_sandbox", + sandboxDir, + "/path/to/bad-symlink", + "", + errors.New("'/path/to/bad-symlink' is outside of sandbox"), + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { + result, err := sandboxedPath(tc.sandbox, tc.path) + if tc.expectedErr != nil { + if err == nil { + t.Fatalf("expected error %s got nil", tc.expectedErr) + } + if err.Error() != tc.expectedErr.Error() { + t.Fatalf("expected %s got %s", tc.expectedErr, err) + } + } + if result != tc.expectedPath { + t.Fatalf("expected %s got %s", tc.expectedPath, result) + } + }) + } +} diff --git a/template/template.go b/template/template.go index 3c9552bb5..ff3f36f21 100644 --- a/template/template.go +++ b/template/template.go @@ -49,6 +49,11 @@ type Template struct { // functionBlacklist are functions not permitted to be executed // when we render this template functionBlacklist []string + + // sandboxPath adds a prefix to any path provided to the `file` function + // and causes an error if a relative path tries to traverse outside that + // prefix. + sandboxPath string } // NewTemplateInput is used as input when creating the template. @@ -70,6 +75,11 @@ type NewTemplateInput struct { // FunctionBlacklist are functions not permitted to be executed // when we render this template FunctionBlacklist []string + + // SandboxPath adds a prefix to any path provided to the `file` function + // and causes an error if a relative path tries to traverse outside that + // prefix. + SandboxPath string } // NewTemplate creates and parses a new Consul Template template at the given @@ -95,6 +105,7 @@ func NewTemplate(i *NewTemplateInput) (*Template, error) { t.rightDelim = i.RightDelim t.errMissingKey = i.ErrMissingKey t.functionBlacklist = i.FunctionBlacklist + t.sandboxPath = i.SandboxPath if i.Source != "" { contents, err := ioutil.ReadFile(i.Source) @@ -138,10 +149,6 @@ type ExecuteInput struct { // Values specified here will take precedence over any values in the // environment when using the `env` function. Env []string - - // BlacklistedFunctions is a set of functions to be disabled - // when executing the template - BlacklistedFunctions []string } // ExecuteResult is the result of the template execution. @@ -174,6 +181,7 @@ func (t *Template) Execute(i *ExecuteInput) (*ExecuteResult, error) { used: &used, missing: &missing, functionBlacklist: t.functionBlacklist, + sandboxPath: t.sandboxPath, })) if t.errMissingKey { @@ -206,6 +214,7 @@ type funcMapInput struct { brain *Brain env []string functionBlacklist []string + sandboxPath string used *dep.Set missing *dep.Set } @@ -217,7 +226,7 @@ func funcMap(i *funcMapInput) template.FuncMap { r := template.FuncMap{ // API functions "datacenters": datacentersFunc(i.brain, i.used, i.missing), - "file": fileFunc(i.brain, i.used, i.missing), + "file": fileFunc(i.brain, i.used, i.missing, i.sandboxPath), "key": keyFunc(i.brain, i.used, i.missing), "keyExists": keyExistsFunc(i.brain, i.used, i.missing), "keyOrDefault": keyWithDefaultFunc(i.brain, i.used, i.missing), diff --git a/template/testdata/sandbox/path/to/bad-symlink b/template/testdata/sandbox/path/to/bad-symlink new file mode 120000 index 000000000..247e762e5 --- /dev/null +++ b/template/testdata/sandbox/path/to/bad-symlink @@ -0,0 +1 @@ +../../../../funcs_test.go \ No newline at end of file diff --git a/template/testdata/sandbox/path/to/file b/template/testdata/sandbox/path/to/file new file mode 100644 index 000000000..e69de29bb diff --git a/template/testdata/sandbox/path/to/ok-symlink b/template/testdata/sandbox/path/to/ok-symlink new file mode 120000 index 000000000..1a010b1c0 --- /dev/null +++ b/template/testdata/sandbox/path/to/ok-symlink @@ -0,0 +1 @@ +file \ No newline at end of file