Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add path sandboxing to file function #1249

Merged
merged 2 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand All @@ -298,6 +311,7 @@ func (c *TemplateConfig) GoString() string {
"LeftDelim:%s, "+
"RightDelim:%s"+
"FunctionBlacklist:%s"+
"SandboxPath:%s"+
"}",
BoolGoString(c.Backup),
StringGoString(c.Command),
Expand All @@ -313,6 +327,7 @@ func (c *TemplateConfig) GoString() string {
StringGoString(c.LeftDelim),
StringGoString(c.RightDelim),
c.FunctionBlacklist,
StringGoString(c.SandboxPath),
)
}

Expand Down
5 changes: 3 additions & 2 deletions config/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(""),
},
},
}
Expand Down
5 changes: 3 additions & 2 deletions manager/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down
28 changes: 26 additions & 2 deletions template/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strconv"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1161,3 +1165,23 @@ 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.Rel(sandbox, path)
if err != nil {
return "", err
}
if strings.HasPrefix(path, "..") {
return "", fmt.Errorf("'%s' is outside of sandbox", s)
}
}
return path, nil
}
74 changes: 74 additions & 0 deletions template/funcs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package template

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/pkg/errors"
)

func TestFileSandbox(t *testing.T) {
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",
"/sandbox",
"/path/to/file",
"/sandbox/path/to/file",
nil,
},
{
"relative_path_in_sandbox",
"/sandbox",
"./path/to/file",
"/sandbox/path/to/file",
nil,
},
{
"relative_path_escaping_sandbox",
"/sandbox",
"/path/../../../to/file",
"",
errors.New("'/path/../../../to/file' 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)
}
})
}
}
19 changes: 14 additions & 5 deletions template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

// ExecuteResult is the result of the template execution.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -206,6 +214,7 @@ type funcMapInput struct {
brain *Brain
env []string
functionBlacklist []string
sandboxPath string
used *dep.Set
missing *dep.Set
}
Expand All @@ -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),
Expand Down