Commit 0aa24266 authored by Tim Gross's avatar Tim Gross
Browse files

variables: restrict allowed paths for variables

Restrict variable paths to RFC3986 URL-safe characters that don't conflict with
the use of characters "@" and "." in `template` blocks. This prevents users from
writing variables that will require tricky templating syntax or that they simply
won't be able to use.

Also restrict the length so that a user can't make queries in the state store
unusually expensive (as they are O(k) on the key length).
parent a62a6542
Showing with 23 additions and 6 deletions
+23 -6
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"strings" "strings"
) )
...@@ -145,12 +146,25 @@ func (sv VariableData) Copy() VariableData { ...@@ -145,12 +146,25 @@ func (sv VariableData) Copy() VariableData {
} }
} }
func (sv VariableDecrypted) Validate() error { var (
// validVariablePath is used to validate a variable path. We restrict to
// RFC3986 URL-safe characters that don't conflict with the use of
// characters "@" and "." in template blocks. We also restrict the length so
// that a user can't make queries in the state store unusually expensive (as
// they are O(k) on the key length)
validVariablePath = regexp.MustCompile("^[a-zA-Z0-9-_~/]{1,128}$")
)
func (v VariableDecrypted) Validate() error {
if len(sv.Path) == 0 { if len(v.Path) == 0 {
return fmt.Errorf("variable requires path") return fmt.Errorf("variable requires path")
} }
parts := strings.Split(sv.Path, "/") if !validVariablePath.MatchString(v.Path) {
return fmt.Errorf("invalid path %q", v.Path)
}
parts := strings.Split(v.Path, "/")
switch { switch {
case len(parts) == 1 && parts[0] == "nomad": case len(parts) == 1 && parts[0] == "nomad":
return fmt.Errorf("\"nomad\" is a reserved top-level directory path, but you may write variables to \"nomad/jobs\" or below") return fmt.Errorf("\"nomad\" is a reserved top-level directory path, but you may write variables to \"nomad/jobs\" or below")
...@@ -158,13 +172,13 @@ func (sv VariableDecrypted) Validate() error { ...@@ -158,13 +172,13 @@ func (sv VariableDecrypted) Validate() error {
return fmt.Errorf("only paths at \"nomad/jobs\" or below are valid paths under the top-level \"nomad\" directory") return fmt.Errorf("only paths at \"nomad/jobs\" or below are valid paths under the top-level \"nomad\" directory")
} }
if len(sv.Items) == 0 { if len(v.Items) == 0 {
return errors.New("empty variables are invalid") return errors.New("empty variables are invalid")
} }
if sv.Items.Size() > maxVariableSize { if v.Items.Size() > maxVariableSize {
return errors.New("variables are limited to 16KiB in total size") return errors.New("variables are limited to 16KiB in total size")
} }
if sv.Namespace == AllNamespacesSentinel { if v.Namespace == AllNamespacesSentinel {
return errors.New("can not target wildcard (\"*\")namespace") return errors.New("can not target wildcard (\"*\")namespace")
} }
return nil return nil
......
...@@ -51,6 +51,9 @@ func TestStructs_VariableDecrypted_Validate(t *testing.T) { ...@@ -51,6 +51,9 @@ func TestStructs_VariableDecrypted_Validate(t *testing.T) {
{path: "nomad/jobs", ok: true}, {path: "nomad/jobs", ok: true},
{path: "nomadjobs", ok: true}, {path: "nomadjobs", ok: true},
{path: "nomad/jobs/whatever", ok: true}, {path: "nomad/jobs/whatever", ok: true},
{path: "example/_-~/whatever", ok: true},
{path: "example/@whatever"},
{path: "example/what.ever"},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc tc := tc
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment