From 82f181338f011f18b032edebf3b58336ffb6adca Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sun, 25 Oct 2026 13:47:38 -0400 Subject: [PATCH 1/4] fix(compose): feed the cache password to the server on stdin so it never sits on argv (D-0587) --- internal/build/secrets_template_test.go | 2 +- internal/compose/cache_service.go | 27 ++++-- internal/compose/cache_service_test.go | 2 +- internal/compose/compose_extra_test.go | 2 +- internal/compose/secret_argv_expanded_test.go | 97 +++++++++++++++++++ internal/compose/secret_argv_test.go | 4 +- internal/compose/secret_literal_test.go | 4 +- 7 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 internal/compose/secret_argv_expanded_test.go diff --git a/internal/build/secrets_template_test.go b/internal/build/secrets_template_test.go index f80e320ca..d548d11b1 100644 --- a/internal/build/secrets_template_test.go +++ b/internal/build/secrets_template_test.go @@ -154,7 +154,7 @@ func TestTemplateSecrets_CommandAndHealthcheckLiteralsRewritten(t *testing.T) { } for _, ref := range []string{ "REDISCLI_AUTH: ${REDIS_PASSWORD}", - `--requirepass "$$REDISCLI_AUTH"`, + `requirepass "$$esc"`, "TYPESENSE_API_KEY: ${TYPESENSE_API_KEY}", } { if !strings.Contains(doc, ref) { diff --git a/internal/compose/cache_service.go b/internal/compose/cache_service.go index a14e63a8a..9f5b6054f 100644 --- a/internal/compose/cache_service.go +++ b/internal/compose/cache_service.go @@ -141,6 +141,17 @@ func (g *Generator) cacheImage(engine string) string { return ResolveImage("redis", ImageRef("redis", version)) } +// redisPasswordScript is the `sh -c` body that starts the cache server with the +// password from $REDISCLI_AUTH on stdin as a `requirepass` config line. The +// value is escaped for the config parser (backslash and double quote), so any +// password round-trips. exec keeps the server as PID 1 for signal handling; +// $$ survives compose interpolation as $. +func redisPasswordScript(server string, poolSize int) string { + return fmt.Sprintf(`esc=$$(printf '%%s' "$$REDISCLI_AUTH" | sed 's/[\\"]/\\&/g'); `+ + "exec %s --appendonly yes --protected-mode yes --maxclients %d - <` and host `ps` or /proc//cmdline shows +// it. This test runs the generated command with a stub server and reads the +// argv the server really receives. + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/nself-org/cli/internal/build" + "github.com/nself-org/cli/internal/compat/compattest" + "github.com/nself-org/cli/internal/compose" +) + +// awkwardPassword holds every character the config line has to escape or the +// shell could mangle. +const awkwardPassword = "p\"a\\ss w$o`rd'x;#9" + +// runCacheCommand runs the sh -c script with a stub engine binary on PATH and +// returns what the stub saw: its argv (one argument per line) and its stdin. +func runCacheCommand(t *testing.T, server, script string) (argv, stdin string) { + t.Helper() + dir := t.TempDir() + stub := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"" + dir + "/argv\"\ncat > \"" + dir + "/stdin\"\n" + if err := os.WriteFile(filepath.Join(dir, server), []byte(stub), 0o755); err != nil { + t.Fatal(err) + } + // compose turns $$ into $ before the container shell sees the script. + cmd := exec.Command("sh", "-c", strings.ReplaceAll(script, "$$", "$")) + cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "REDISCLI_AUTH="+awkwardPassword) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("run %q: %v\n%s", script, err, out) + } + a, err := os.ReadFile(filepath.Join(dir, "argv")) + if err != nil { + t.Fatal(err) + } + s, err := os.ReadFile(filepath.Join(dir, "stdin")) + if err != nil { + t.Fatal(err) + } + return string(a), string(s) +} + +// TestExpandedCacheCommandNoSecretOnArgv: the expanded server argv carries no +// part of the password and the server still receives it as a config line. +func TestExpandedCacheCommandNoSecretOnArgv(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("needs a POSIX sh") + } + compattest.Set(t, false) + for _, cache := range []string{"redis", "valkey"} { + cfg := optionalSecretsConfig("meilisearch", setSecrets) + cfg.Redis.Engine = cache + cfg.Redis.Password = awkwardPassword + out, err := compose.NewGenerator(cfg).Generate() + if err != nil { + t.Fatalf("%s: Generate() error: %v", cache, err) + } + cmd := argvStrings(t, build.TemplateSecrets(out, build.SecretEnvMap(cfg)))["redis command"] + if len(cmd) != 3 || cmd[0] != "sh" || cmd[1] != "-c" { + t.Fatalf("%s: redis command = %q, want sh -c