From 07dcb4094158639bad9623dd6f92576dd86e99bb Mon Sep 17 00:00:00 2001 From: Arunesh Dwivedi Date: Mon, 20 Jul 2026 06:46:22 +0000 Subject: [PATCH] fix: preserve literal dollar signs in config values readFileAndExpandEnv used os.ExpandEnv which strips bare $ characters from values such as passwords, corrupting them. Expand only the ${VAR} form so intentional environment substitution still works while literal dollar signs are preserved. Fixes #88 Signed-off-by: Arunesh Dwivedi --- cmd/remco/config.go | 19 +++++++++- cmd/remco/config_env_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 cmd/remco/config_env_test.go diff --git a/cmd/remco/config.go b/cmd/remco/config.go index b8b4e97..ec52e19 100644 --- a/cmd/remco/config.go +++ b/cmd/remco/config.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "strings" "github.com/BurntSushi/toml" @@ -23,6 +24,9 @@ import ( "github.com/pkg/errors" ) +// bracedEnvRegex matches the ${VAR} form of environment variable references. +var bracedEnvRegex = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}`) + // BackendConfigs holds every individually backend config. // The values are filled with data from the configuration file. type BackendConfigs struct { @@ -129,13 +133,24 @@ type Resource struct { Name string } +// expandEnvBraced expands only the ${VAR} form of environment variables. +// Unlike os.ExpandEnv it leaves a bare "$" untouched, so literal dollar signs +// in configuration values (for example a password like "secret$Q$") are +// preserved instead of being stripped. +func expandEnvBraced(s string) string { + return bracedEnvRegex.ReplaceAllStringFunc(s, func(match string) string { + name := match[2 : len(match)-1] + return os.Getenv(name) + }) +} + func readFileAndExpandEnv(path string) ([]byte, error) { buf, err := ioutil.ReadFile(path) if err != nil { return buf, errors.Wrap(err, "read file failed") } - // expand the environment variables - buf = []byte(os.ExpandEnv(string(buf))) + // expand only ${VAR} style environment variables + buf = []byte(expandEnvBraced(string(buf))) return buf, nil } diff --git a/cmd/remco/config_env_test.go b/cmd/remco/config_env_test.go new file mode 100644 index 0000000..636e4d0 --- /dev/null +++ b/cmd/remco/config_env_test.go @@ -0,0 +1,71 @@ +/* + * This file is part of remco. + * © 2016 The Remco Authors + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// TestReadFileAndExpandEnv verifies that configuration values containing a +// literal "$" (for example a password like "uhQhu4watyTgn$Q$") are not +// corrupted, while the ${VAR} form is still expanded. +// +// It exercises readFileAndExpandEnv, the function that actually parses the +// on-disk config, so it fails on the previous os.ExpandEnv implementation +// (which stripped the "$Q$" portion) and passes on the braced-only expander. +func TestReadFileAndExpandEnv(t *testing.T) { + os.Setenv("REMCO_TEST_TOKEN", "s3cr3t") + defer os.Unsetenv("REMCO_TEST_TOKEN") + + cases := []struct { + name string + in string + want string + }{ + { + name: "braced var is expanded", + in: "password = \"${REMCO_TEST_TOKEN}\"", + want: "password = \"s3cr3t\"", + }, + { + name: "literal dollar signs are preserved", + in: "password = \"uhQhu4watyTgn$Q$\"", + want: "password = \"uhQhu4watyTgn$Q$\"", + }, + { + name: "bare dollar without braces is preserved", + in: "value = \"costs $5 today\"", + want: "value = \"costs $5 today\"", + }, + { + name: "mixing braced var and literal dollar", + in: "conn = \"${REMCO_TEST_TOKEN}:uhQhu4watyTgn$Q$\"", + want: "conn = \"s3cr3t:uhQhu4watyTgn$Q$\"", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "config.toml") + if err := os.WriteFile(p, []byte(tc.in), 0o644); err != nil { + t.Fatal(err) + } + got, err := readFileAndExpandEnv(p) + if err != nil { + t.Fatal(err) + } + if string(got) != tc.want { + t.Errorf("readFileAndExpandEnv(%q) = %q, want %q", tc.in, string(got), tc.want) + } + }) + } +}