Skip to content

Commit

Permalink
template: disallow writeToFile by default
Browse files Browse the repository at this point in the history
Resolves #12095 by WONTFIXing it.

This approach disables `writeToFile` as it allows arbitrary host
filesystem writes and is only a small quality of life improvement over
multiple `template` stanzas.

This approach has the significant downside of leaving people who have
altered their `template.function_denylist` *still vulnerable!* I added
an upgrade note, but we should have implemented the denylist as a
`map[string]bool` so that new funcs could be denied without overriding
custom configurations.

This PR also includes a bug fix that broke enabling all consul-template
funcs. We repeatedly failed to differentiate between a nil (unset)
denylist and an empty (allow all) one.
  • Loading branch information
schmichael committed Mar 18, 2022
1 parent 74c8860 commit 31ae1ff
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 105 deletions.
6 changes: 6 additions & 0 deletions .changelog/12312.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:bug
template: Fixed a bug preventing allowing all consul-template functions.
```
```release-note:improvement
template: Upgraded to from consul-template v0.25.2 to v0.28.0 which includes the sprig library of functions and more.
```
108 changes: 104 additions & 4 deletions client/allocrunner/taskrunner/template/template_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package template

import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -55,7 +58,7 @@ type MockTaskHooks struct {
UnblockCh chan struct{}

KillEvent *structs.TaskEvent
KillCh chan struct{}
KillCh chan *structs.TaskEvent

Events []*structs.TaskEvent
EmitEventCh chan *structs.TaskEvent
Expand All @@ -69,7 +72,7 @@ func NewMockTaskHooks() *MockTaskHooks {
UnblockCh: make(chan struct{}, 1),
RestartCh: make(chan struct{}, 1),
SignalCh: make(chan struct{}, 1),
KillCh: make(chan struct{}, 1),
KillCh: make(chan *structs.TaskEvent, 1),
EmitEventCh: make(chan *structs.TaskEvent, 1),
}
}
Expand Down Expand Up @@ -97,7 +100,7 @@ func (m *MockTaskHooks) Signal(event *structs.TaskEvent, s string) error {
func (m *MockTaskHooks) Kill(ctx context.Context, event *structs.TaskEvent) error {
m.KillEvent = event
select {
case m.KillCh <- struct{}{}:
case m.KillCh <- event:
default:
}
return nil
Expand Down Expand Up @@ -145,7 +148,7 @@ func newTestHarness(t *testing.T, templates []*structs.Template, consul, vault b
config: &config.Config{
Region: region,
TemplateConfig: &config.ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: config.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
ConsulRetry: &config.RetryConfig{Backoff: helper.TimeToPtr(10 * time.Millisecond)},
}},
Expand Down Expand Up @@ -2157,3 +2160,100 @@ func TestTaskTemplateManager_Template_Wait_Set(t *testing.T) {
require.Equal(t, 10*time.Second, *k.Wait.Max)
}
}

// TestTaskTemplateManager_writeToFile_Disabled asserts the consul-template function
// writeToFile is disabled by default.
func TestTaskTemplateManager_writeToFile_Disabled(t *testing.T) {
ci.Parallel(t)

file := "my.tmpl"
template := &structs.Template{
EmbeddedTmpl: `Testing writeToFile...
{{ "if i exist writeToFile is enabled" | writeToFile "/tmp/NOMAD-TEST-SHOULD-NOT-EXIST" "" "" "0644" }}
...done
`,
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}

harness := newTestHarness(t, []*structs.Template{template}, false, false)
require.NoError(t, harness.startWithErr(), "couldn't setup initial harness")
defer harness.stop()

// Using writeToFile should cause a kill
select {
case <-harness.mockHooks.UnblockCh:
t.Fatalf("Task unblock should have not have been called")
case <-harness.mockHooks.EmitEventCh:
t.Fatalf("Task event should not have been emitted")
case e := <-harness.mockHooks.KillCh:
require.Contains(t, e.DisplayMessage, "writeToFile: function is disabled")
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatalf("timeout")
}

// Check the file is not there
path := filepath.Join(harness.taskDir, file)
_, err := ioutil.ReadFile(path)
require.Error(t, err)
}

// TestTaskTemplateManager_writeToFile asserts the consul-template function
// writeToFile can be enabled.
func TestTaskTemplateManager_writeToFile(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("username and group lookup assume linux platform")
}

ci.Parallel(t)

cu, err := user.Current()
require.NoError(t, err)

cg, err := user.LookupGroupId(cu.Gid)
require.NoError(t, err)

file := "my.tmpl"
template := &structs.Template{
// EmbeddedTmpl set below as it needs the taskDir
DestPath: file,
ChangeMode: structs.TemplateChangeModeNoop,
}

harness := newTestHarness(t, []*structs.Template{template}, false, false)

// Add template now that we know the taskDir
harness.templates[0].EmbeddedTmpl = fmt.Sprintf(`Testing writeToFile...
{{ "hello" | writeToFile "%s" "`+cu.Username+`" "`+cg.Name+`" "0644" }}
...done
`, filepath.Join(harness.taskDir, "writetofile.out"))

// Enable all funcs
harness.config.TemplateConfig.FunctionDenylist = []string{}

require.NoError(t, harness.startWithErr(), "couldn't setup initial harness")
defer harness.stop()

// Using writeToFile should cause a kill
select {
case <-harness.mockHooks.UnblockCh:
case <-harness.mockHooks.EmitEventCh:
t.Fatalf("Task event should not have been emitted")
case e := <-harness.mockHooks.KillCh:
t.Fatalf("Task should not have been killed: %v", e.DisplayMessage)
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
t.Fatalf("timeout")
}

// Check the templated file is there
path := filepath.Join(harness.taskDir, file)
r, err := ioutil.ReadFile(path)
require.NoError(t, err)
require.True(t, bytes.HasSuffix(r, []byte("...done\n")), string(r))

// Check that writeToFile was allowed
path = filepath.Join(harness.taskDir, "writetofile.out")
r, err = ioutil.ReadFile(path)
require.NoError(t, err)
require.Equal(t, "hello", string(r))
}
22 changes: 18 additions & 4 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ var (
}

DefaultTemplateMaxStale = 5 * time.Second

DefaultTemplateFunctionDenylist = []string{"plugin", "writeToFile"}
)

// RPCHandler can be provided to the Client if there is a local server
Expand Down Expand Up @@ -354,7 +356,13 @@ func (c *ClientTemplateConfig) Copy() *ClientTemplateConfig {

nc := new(ClientTemplateConfig)
*nc = *c
nc.FunctionDenylist = helper.CopySliceString(nc.FunctionDenylist)

if len(c.FunctionDenylist) > 0 {
nc.FunctionDenylist = helper.CopySliceString(nc.FunctionDenylist)
} else if c.FunctionDenylist != nil {
// Explicitly no functions denied (which is different than nil)
nc.FunctionDenylist = []string{}
}

if c.BlockQueryWaitTime != nil {
nc.BlockQueryWaitTime = &*c.BlockQueryWaitTime
Expand Down Expand Up @@ -412,6 +420,9 @@ func (c *ClientTemplateConfig) Merge(b *ClientTemplateConfig) *ClientTemplateCon
result.FunctionBlacklist = append(result.FunctionBlacklist, fn)
}
}
} else if b.FunctionBlacklist != nil {
// No funcs denied
result.FunctionBlacklist = []string{}
}

if len(b.FunctionDenylist) > 0 {
Expand All @@ -420,6 +431,9 @@ func (c *ClientTemplateConfig) Merge(b *ClientTemplateConfig) *ClientTemplateCon
result.FunctionDenylist = append(result.FunctionDenylist, fn)
}
}
} else if b.FunctionDenylist != nil {
// No funcs denied
result.FunctionDenylist = []string{}
}

if b.MaxStale != nil {
Expand Down Expand Up @@ -451,8 +465,8 @@ func (c *ClientTemplateConfig) IsEmpty() bool {
}

return !c.DisableSandbox &&
len(c.FunctionDenylist) == 0 &&
len(c.FunctionBlacklist) == 0 &&
c.FunctionDenylist == nil &&
c.FunctionBlacklist == nil &&
c.BlockQueryWaitTime == nil &&
c.BlockQueryWaitTimeHCL == "" &&
c.MaxStale == nil &&
Expand Down Expand Up @@ -766,7 +780,7 @@ func DefaultConfig() *Config {
NoHostUUID: true,
DisableRemoteExec: false,
TemplateConfig: &ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
RPCHoldTimeout: 5 * time.Second,
Expand Down
4 changes: 2 additions & 2 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ func DevConfig(mode *devModeConfig) *Config {
conf.Client.GCInodeUsageThreshold = 99
conf.Client.GCMaxAllocs = 50
conf.Client.TemplateConfig = &client.ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
}
conf.Client.BindWildcardDefaultHostNetwork = true
Expand Down Expand Up @@ -960,7 +960,7 @@ func DefaultConfig() *Config {
RetryMaxAttempts: 0,
},
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
BindWildcardDefaultHostNetwork: true,
Expand Down
10 changes: 6 additions & 4 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func TestConfig_Merge(t *testing.T) {
ClientMaxPort: 19996,
DisableRemoteExec: false,
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
Reserved: &Resources{
Expand Down Expand Up @@ -304,7 +304,7 @@ func TestConfig_Merge(t *testing.T) {
MaxKillTimeout: "50s",
DisableRemoteExec: false,
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: []string{"plugin"},
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
Reserved: &Resources{
Expand Down Expand Up @@ -1463,6 +1463,8 @@ func TestConfig_LoadConsulTemplateBasic(t *testing.T) {
require.NotNil(t, agentConfig.Client.TemplateConfig)

agentConfig = defaultConfig.Merge(agentConfig)
require.Len(t, agentConfig.Client.TemplateConfig.FunctionDenylist, 0)
require.NotNil(t, agentConfig.Client.TemplateConfig.FunctionDenylist)

clientAgent := Agent{config: agentConfig}
clientConfig, err := clientAgent.clientConfig()
Expand All @@ -1471,7 +1473,7 @@ func TestConfig_LoadConsulTemplateBasic(t *testing.T) {
templateConfig := clientConfig.TemplateConfig
require.NotNil(t, templateConfig)
require.True(t, templateConfig.DisableSandbox)
require.Len(t, templateConfig.FunctionDenylist, 1)
require.Len(t, templateConfig.FunctionDenylist, 0)

// json
agentConfig, err = LoadConfig("test-resources/client_with_basic_template.json")
Expand All @@ -1486,7 +1488,7 @@ func TestConfig_LoadConsulTemplateBasic(t *testing.T) {
templateConfig = clientConfig.TemplateConfig
require.NotNil(t, templateConfig)
require.True(t, templateConfig.DisableSandbox)
require.Len(t, templateConfig.FunctionDenylist, 1)
require.Len(t, templateConfig.FunctionDenylist, 0)
}

func TestParseMultipleIPTemplates(t *testing.T) {
Expand Down
Loading

0 comments on commit 31ae1ff

Please sign in to comment.