artifact/template: prevent file sandbox escapes

Ensure that the client honors the client configuration for the
`template.disable_file_sandbox` field when validating the jobspec's
`template.source` parameter, and not just with consul-template's own `file`
function.

Prevent interpolated `template.source`, `template.destination`, and
`artifact.destination` fields from escaping file sandbox.
This commit is contained in:
Tim Gross
2020-10-16 13:23:44 -04:00
parent d552b4e8d1
commit 076db2ef6b
6 changed files with 182 additions and 23 deletions

View File

@@ -28,10 +28,6 @@ const (
// consulTemplateSourceName is the source name when using the TaskHooks.
consulTemplateSourceName = "Template"
// hostSrcOption is the Client option that determines whether the template
// source may be from the host
hostSrcOption = "template.allow_host_source"
// missingDepEventLimit is the number of missing dependencies that will be
// logged before we switch to showing just the number of missing
// dependencies.
@@ -549,25 +545,27 @@ func maskProcessEnv(env map[string]string) map[string]string {
// parseTemplateConfigs converts the tasks templates in the config into
// consul-templates
func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.TemplateConfig]*structs.Template, error) {
allowAbs := config.ClientConfig.ReadBoolDefault(hostSrcOption, true)
sandboxEnabled := !config.ClientConfig.TemplateConfig.DisableSandbox
taskEnv := config.EnvBuilder.Build()
ctmpls := make(map[*ctconf.TemplateConfig]*structs.Template, len(config.Templates))
for _, tmpl := range config.Templates {
var src, dest string
var err error
if tmpl.SourcePath != "" {
if filepath.IsAbs(tmpl.SourcePath) {
if !allowAbs {
return nil, fmt.Errorf("Specifying absolute template paths disallowed by client config: %q", tmpl.SourcePath)
}
src = tmpl.SourcePath
} else {
src = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.SourcePath))
src = taskEnv.ReplaceEnv(tmpl.SourcePath)
src, err = helper.GetPathInSandbox(config.TaskDir, src)
if err != nil && sandboxEnabled {
return nil, fmt.Errorf("template source path escapes alloc directory")
}
}
if tmpl.DestPath != "" {
dest = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.DestPath))
dest = taskEnv.ReplaceEnv(tmpl.DestPath)
dest, err = helper.GetPathInSandbox(config.TaskDir, dest)
if err != nil && sandboxEnabled {
return nil, fmt.Errorf("template destination path escapes alloc directory")
}
}
ct := ctconf.DefaultTemplateConfig()
@@ -577,7 +575,7 @@ func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.Templa
ct.LeftDelim = &tmpl.LeftDelim
ct.RightDelim = &tmpl.RightDelim
ct.FunctionDenylist = config.ClientConfig.TemplateConfig.FunctionDenylist
if !config.ClientConfig.TemplateConfig.DisableSandbox {
if sandboxEnabled {
ct.SandboxPath = &config.TaskDir
}

View File

@@ -382,7 +382,11 @@ func TestTaskTemplateManager_HostPath(t *testing.T) {
}
harness := newTestHarness(t, []*structs.Template{template}, false, false)
harness.start(t)
harness.config.TemplateConfig.DisableSandbox = true
err = harness.startWithErr()
if err != nil {
t.Fatalf("couldn't setup initial harness: %v", err)
}
defer harness.stop()
// Wait for the unblock
@@ -405,12 +409,46 @@ func TestTaskTemplateManager_HostPath(t *testing.T) {
// Change the config to disallow host sources
harness = newTestHarness(t, []*structs.Template{template}, false, false)
harness.config.Options = map[string]string{
hostSrcOption: "false",
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected absolute template path disallowed for %q: %v",
template.SourcePath, err)
}
if err := harness.startWithErr(); err == nil || !strings.Contains(err.Error(), "absolute") {
t.Fatalf("Expected absolute template path disallowed: %v", err)
template.SourcePath = "../../../../../../" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected directory traversal out of %q disallowed for %q: %v",
harness.taskDir, template.SourcePath, err)
}
// Build a new task environment
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
task.Name = TestTaskName
task.Meta = map[string]string{"ESCAPE": "../"}
template.SourcePath = "${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}${NOMAD_META_ESCAPE}" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v",
harness.taskDir, template.SourcePath, err)
}
// Test with desination too
template.SourcePath = f.Name()
template.DestPath = "../../../../../../" + file
harness = newTestHarness(t, []*structs.Template{template}, false, false)
harness.envBuilder = taskenv.NewBuilder(harness.node, a, task, "global")
err = harness.startWithErr()
if err == nil || !strings.Contains(err.Error(), "escapes alloc directory") {
t.Fatalf("Expected directory traversal out of %q via interpolation disallowed for %q: %v",
harness.taskDir, template.SourcePath, err)
}
}
func TestTaskTemplateManager_Unblock_Static(t *testing.T) {