diff --git a/engine/pkg/config/envvar/envvar.go b/engine/pkg/config/envvar/envvar.go index 4b574e5e..8962cb56 100644 --- a/engine/pkg/config/envvar/envvar.go +++ b/engine/pkg/config/envvar/envvar.go @@ -1,9 +1,12 @@ -// Package envvar expands "${VAR}" and "$VAR" placeholders in selected config -// fields using the process environment. +// Package envvar expands a config field whose value is solely a "${VAR}" or +// "$VAR" placeholder using the process environment. A value that merely +// contains a "$" (a password, a regex backreference, an unmatched brace) is a +// literal and is left unchanged. package envvar import ( "os" + "regexp" "github.com/pkg/errors" ) @@ -14,35 +17,35 @@ type Field struct { Ptr *string } -// ExpandStrict expands "${VAR}" and "$VAR" references in s. It returns an -// error if any referenced variable is unset, so misconfigured tokens fail -// loudly at startup instead of silently resolving to an empty string. +// placeholderRE matches a value that consists solely of a single "${VAR}" or +// "$VAR" reference, capturing the variable name. Anything else (including a +// value that merely embeds a "$") is treated as a literal so secrets that +// legitimately contain a "$" survive load without corruption. +var placeholderRE = regexp.MustCompile(`^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$|^\$([A-Za-z_][A-Za-z0-9_]*)$`) + +// ExpandStrict resolves s when it is exactly a single "${VAR}" or "$VAR" +// placeholder, returning the referenced environment variable's value. It +// returns an error if the referenced variable is unset, so a misconfigured +// placeholder fails loudly at startup instead of silently resolving to an +// empty string. Any other value, including one that merely contains a "$", is +// returned unchanged. func ExpandStrict(s string) (string, error) { - if s == "" { - return "", nil + m := placeholderRE.FindStringSubmatch(s) + if m == nil { + return s, nil } - var missing string - - resolved := os.Expand(s, func(name string) string { - if missing != "" { - return "" - } - - v, ok := os.LookupEnv(name) - if !ok { - missing = name - return "" - } - - return v - }) + name := m[1] + if name == "" { + name = m[2] + } - if missing != "" { - return "", errors.Errorf("environment variable %q is not set", missing) + v, ok := os.LookupEnv(name) + if !ok { + return "", errors.Errorf("environment variable %q is not set", name) } - return resolved, nil + return v, nil } // ExpandFields applies ExpandStrict to each field and writes the resolved diff --git a/engine/pkg/config/envvar/envvar_test.go b/engine/pkg/config/envvar/envvar_test.go index 0c6d234e..c9d631db 100644 --- a/engine/pkg/config/envvar/envvar_test.go +++ b/engine/pkg/config/envvar/envvar_test.go @@ -10,6 +10,7 @@ import ( func TestExpandStrict(t *testing.T) { t.Setenv("DBLAB_TOKEN", "secret-from-env") t.Setenv("EMPTY_VAR", "") + t.Setenv("en", "injected-value") tests := []struct { name string @@ -23,7 +24,13 @@ func TestExpandStrict(t *testing.T) { {name: "unbraced placeholder", input: "$DBLAB_TOKEN", want: "secret-from-env"}, {name: "explicitly empty env var", input: "${EMPTY_VAR}", want: ""}, {name: "unset variable", input: "${DBLAB_MISSING}", wantErr: `environment variable "DBLAB_MISSING" is not set`}, - {name: "regex backreference looks like unset var", input: "***$1", wantErr: `environment variable "1" is not set`}, + {name: "regex backreference preserved", input: "***$1", want: "***$1"}, + {name: "password with dollar preserved", input: "pa$$w0rd", want: "pa$$w0rd"}, + {name: "embedded reference not expanded", input: "tok$en", want: "tok$en"}, + {name: "leading dollar embedded preserved", input: "$DBLAB_TOKEN-suffix", want: "$DBLAB_TOKEN-suffix"}, + {name: "unclosed brace preserved", input: "${UNCLOSED", want: "${UNCLOSED"}, + {name: "empty braces preserved", input: "${}", want: "${}"}, + {name: "bare dollar preserved", input: "$", want: "$"}, } for _, tc := range tests {