From 0f5d1b3ac8f0986e81258c532269992de5229c28 Mon Sep 17 00:00:00 2001 From: Roberto Carlos Luzanilla Sanchez Date: Mon, 2 Mar 2026 12:32:04 -0700 Subject: [PATCH 1/3] execute: avoid interpolating args into bash -c --- internal/execute/execute.go | 25 ++++++++----------------- internal/execute/execute_test.go | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/internal/execute/execute.go b/internal/execute/execute.go index 51fd66bca..5644007fd 100644 --- a/internal/execute/execute.go +++ b/internal/execute/execute.go @@ -36,17 +36,18 @@ func NewExpression(expression string, args []string) Command { // Run executes a Command with Bash and returns the error if there is one func (c Command) Run() error { - var command string + var bashArgs []string + if c.Expression != "" { - // Expressions need to be invoked inside a Bash function, so variables like - // $0 and $@ are available - command = fmt.Sprintf("fn() { %s; }; fn %s", c.Expression, formatArgString(c.Args)) + script := fmt.Sprintf(`fn() { %s; }; fn "$@"`, c.Expression) + bashArgs = append([]string{"-c", script, "asdf"}, c.Args...) + } else if len(c.Args) > 0 { + bashArgs = append([]string{"-c", `exec "$0" "$@"`, c.Command}, c.Args...) } else { - // Scripts can be invoked directly, with args provided - command = fmt.Sprintf("%s %s", c.Command, formatArgString(c.Args)) + bashArgs = []string{"-c", c.Command} } - cmd := exec.Command("bash", "-c", command) + cmd := exec.Command("bash", bashArgs...) if len(c.Env) > 0 { cmd.Env = MergeWithCurrentEnv(c.Env) @@ -55,8 +56,6 @@ func (c Command) Run() error { } cmd.Stdin = c.Stdin - - // Capture stdout and stderr cmd.Stdout = c.Stdout cmd.Stderr = c.Stderr @@ -105,11 +104,3 @@ func SliceToMap(env []string) map[string]string { return envMap } - -func formatArgString(args []string) string { - var newArgs []string - for _, str := range args { - newArgs = append(newArgs, fmt.Sprintf("\"%s\"", str)) - } - return strings.Join(newArgs, " ") -} diff --git a/internal/execute/execute_test.go b/internal/execute/execute_test.go index 9f84babe3..6e072b281 100644 --- a/internal/execute/execute_test.go +++ b/internal/execute/execute_test.go @@ -2,6 +2,7 @@ package execute import ( "fmt" + "io" "os" "os/exec" "strings" @@ -18,6 +19,22 @@ func TestNew(t *testing.T) { }) } +func TestRun_NoArgInjection(t *testing.T) { + tmp := t.TempDir() + injected := tmp + "/injected" + + payload := fmt.Sprintf(`http://x"; : > %s #`, injected) + + cmd := New("git", []string{"clone", payload, tmp + "/noop"}) + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + _ = cmd.Run() + + if _, err := os.Stat(injected); err == nil { + t.Fatalf("injection succeeded: %s was created", injected) + } +} + func TestNewExpression(t *testing.T) { t.Run("Returns new command expression", func(t *testing.T) { cmd := NewExpression("echo", []string{"test string"}) From 4f6d37635e4685848333550cbae9704df2dffaee Mon Sep 17 00:00:00 2001 From: Roberto Carlos Luzanilla Sanchez Date: Wed, 4 Mar 2026 18:30:27 -0700 Subject: [PATCH 2/3] fix: prevent CRLF in testdata script and fix shell arg handling --- internal/execute/execute.go | 50 +++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/internal/execute/execute.go b/internal/execute/execute.go index 5644007fd..baf2d521a 100644 --- a/internal/execute/execute.go +++ b/internal/execute/execute.go @@ -1,6 +1,3 @@ -// Package execute is a simple package that wraps the os/exec Command features -// for convenient use in asdf. It was inspired by -// https://github.com/chen-keinan/go-command-eval package execute import ( @@ -36,32 +33,52 @@ func NewExpression(expression string, args []string) Command { // Run executes a Command with Bash and returns the error if there is one func (c Command) Run() error { - var bashArgs []string + var cmd *exec.Cmd if c.Expression != "" { + // Expresiones bash: fn wrapper para que $0/$@ estén disponibles script := fmt.Sprintf(`fn() { %s; }; fn "$@"`, c.Expression) - bashArgs = append([]string{"-c", script, "asdf"}, c.Args...) - } else if len(c.Args) > 0 { - bashArgs = append([]string{"-c", `exec "$0" "$@"`, c.Command}, c.Args...) + args := append([]string{"-c", script, "asdf"}, c.Args...) + cmd = exec.Command("bash", args...) + + } else if isShellExpression(c.Command) || len(c.Args) == 0 { + command := c.Command + if len(c.Args) > 0 { + command = fmt.Sprintf("%s %s", c.Command, formatArgString(c.Args)) + } + cmd = exec.Command("bash", "-c", command) + } else { - bashArgs = []string{"-c", c.Command} + binary := strings.Trim(c.Command, "'\"") + args := append([]string{"-c", `"$0" "$@"`, binary}, c.Args...) + cmd = exec.Command("bash", args...) } - cmd := exec.Command("bash", bashArgs...) - if len(c.Env) > 0 { cmd.Env = MergeWithCurrentEnv(c.Env) } else { cmd.Env = os.Environ() } - cmd.Stdin = c.Stdin cmd.Stdout = c.Stdout cmd.Stderr = c.Stderr - return cmd.Run() } +// isShellExpression detecta si el comando contiene metacaracteres de shell +func isShellExpression(command string) bool { + return strings.ContainsAny(command, "$|;&`(){}[]<>\\") +} + +// formatArgString wraps each argument in double quotes +func formatArgString(args []string) string { + result := []string{} + for _, arg := range args { + result = append(result, fmt.Sprintf(`"%s"`, arg)) + } + return strings.Join(result, " ") +} + // MergeWithCurrentEnv merges the provided map into the current environment variables func MergeWithCurrentEnv(env map[string]string) (slice []string) { return MapToSlice(MergeEnv(CurrentEnv(), env)) @@ -77,7 +94,6 @@ func MergeEnv(map1, map2 map[string]string) map[string]string { for key, value := range map2 { map1[key] = value } - return map1 } @@ -86,21 +102,17 @@ func MapToSlice(env map[string]string) (slice []string) { for key, value := range env { slice = append(slice, fmt.Sprintf("%s=%s", key, value)) } - return slice } -// SliceToMap converts an env map to env slice suitable for syscall.Exec +// SliceToMap converts an env slice to env map func SliceToMap(env []string) map[string]string { envMap := map[string]string{} - for _, envVar := range env { varValue := strings.SplitN(envVar, "=", 2) - if len(varValue) == 2 { envMap[varValue[0]] = varValue[1] } } - return envMap -} +} \ No newline at end of file From e8036fefc942c8dd601aa5d2e3af2ae8f2643f66 Mon Sep 17 00:00:00 2001 From: Roberto Carlos Luzanilla Sanchez Date: Wed, 4 Mar 2026 18:45:54 -0700 Subject: [PATCH 3/3] fix: avoid interpolating structured command args into bash -c --- internal/execute/execute.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/execute/execute.go b/internal/execute/execute.go index baf2d521a..24016e8a9 100644 --- a/internal/execute/execute.go +++ b/internal/execute/execute.go @@ -50,7 +50,7 @@ func (c Command) Run() error { } else { binary := strings.Trim(c.Command, "'\"") - args := append([]string{"-c", `"$0" "$@"`, binary}, c.Args...) + args := append([]string{"-c", `exec "$0" "$@"`, binary}, c.Args...) cmd = exec.Command("bash", args...) }