Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion internal/shellwrap/redact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions internal/shellwrap/redact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading