From 8dcd3b08b719e2093f5ff817866f679434ffe76e Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 23 Oct 2026 21:58:35 -0400 Subject: [PATCH] fix(shellwrap): mask glued docker env forms inside a command string Co-authored-by: Cursor --- internal/shellwrap/redact.go | 19 ++++++++++++++++++- internal/shellwrap/redact_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/shellwrap/redact.go b/internal/shellwrap/redact.go index f1e19113e..9fb5fc462 100644 --- a/internal/shellwrap/redact.go +++ b/internal/shellwrap/redact.go @@ -15,6 +15,12 @@ import ( // flags or image names. var dockerEnvFlagRegex = regexp.MustCompile(`(^|\s)(--env|-e)(\s+)('[^']*'|"[^"]*"|\S+)`) +// dockerGluedEnvRegex matches the glued spellings (`--env=KEY=VALUE`, +// `-eKEY=VALUE`) within a command STRING, either bare or as one quoted word +// (Shellescape wraps the whole token when the value holds whitespace). Group 2 +// is the opening quote, if any; group 3 the flag; group 4 the KEY=VALUE text. +var dockerGluedEnvRegex = regexp.MustCompile(`(^|\s)(?:(')(--env=|-e)([^']*)'|(")(--env=|-e)([^"]*)"|()(--env=|-e)([^\s'"]\S*))`) + // RedactDockerArgs returns a copy of a docker-style argv slice with the VALUE // of every `KEY=VALUE` env injection masked, leaving keys, flags, image names, // and other args untouched. It handles three shapes the spawn path produces: @@ -84,11 +90,22 @@ func RedactDockerCommandString(s string) string { // RedactDockerCommandStringWith is RedactDockerCommandString with a // caller-chosen mask rendering. See RedactDockerArgsWith. func RedactDockerCommandStringWith(s string, mask func(string) string) string { - return dockerEnvFlagRegex.ReplaceAllStringFunc(s, func(m string) string { + s = dockerEnvFlagRegex.ReplaceAllStringFunc(s, func(m string) string { sub := dockerEnvFlagRegex.FindStringSubmatch(m) // sub[1]=lead, sub[2]=flag, sub[3]=whitespace, sub[4]=value token return sub[1] + sub[2] + sub[3] + maskEnvTokenWith(sub[4], mask) }) + return dockerGluedEnvRegex.ReplaceAllStringFunc(s, func(m string) string { + sub := dockerGluedEnvRegex.FindStringSubmatch(m) + // Exactly one of the three (quote, flag, KEY=VALUE) triples matched. + for i := 2; i+2 < len(sub); i += 3 { + if sub[i+1] != "" { + q := sub[i] + return sub[1] + q + sub[i+1] + maskEnvTokenWith(sub[i+2], mask) + q + } + } + return m + }) } // maskEnvTokenWith masks the value portion of a `KEY=VALUE` token, preserving diff --git a/internal/shellwrap/redact_test.go b/internal/shellwrap/redact_test.go index 2a65a651e..3dea22f83 100644 --- a/internal/shellwrap/redact_test.go +++ b/internal/shellwrap/redact_test.go @@ -82,6 +82,36 @@ func TestRedactDockerCommandString(t *testing.T) { } } +// The glued spellings are masked as separate argv elements; they must be +// masked the same way once a login-shell wrap folds them into one -c string. +func TestRedactDockerCommandString_GluedForms(t *testing.T) { + cases := map[string]struct{ in, keep string }{ + "long glued": {"docker run --rm --env=SLACK_TOKEN=" + secretValueSlack + " myimage", "--env=SLACK_TOKEN="}, + "short glued": {"docker run --rm -eSLACK_TOKEN=" + secretValueSlack + " myimage", "-eSLACK_TOKEN="}, + "shellescaped glued": {"docker run --rm '--env=SLACK_TOKEN=" + secretValueSlack + " x' myimage", "'--env=SLACK_TOKEN="}, + "double quoted": {`docker run --rm "-eSLACK_TOKEN=` + secretValueSlack + `" myimage`, `"-eSLACK_TOKEN=`}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + out := RedactDockerCommandString(tc.in) + assertNoSecret(t, out) + for _, want := range []string{"docker run", "--rm", "myimage", tc.keep} { + if !strings.Contains(out, want) { + t.Errorf("expected %q preserved in %q", want, out) + } + } + args := RedactDockerArgs([]string{"-l", "-c", tc.in}) + assertNoSecret(t, strings.Join(args, " ")) + }) + } + // Flags that merely start with -e and carry no KEY=VALUE are untouched. + for _, in := range []string{"node -e console.log(1)", "grep -enum file", "docker run --entrypoint /bin/sh img"} { + if out := RedactDockerCommandString(in); out != in { + t.Errorf("non-env command altered: %q -> %q", in, out) + } + } +} + func TestRedactDockerArgs_NoEnvFlagsUnchanged(t *testing.T) { in := []string{"run", "--rm", "-i", "ghcr.io/foo/bar:latest", "--config", "/etc/x.json"} out := RedactDockerArgs(in)