From 383d28a91a4e98bb7411b27d3d4f33f11d3560ba Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 7 Mar 2026 19:50:06 +0000 Subject: [PATCH] refactor(recipe): enhance shell command security with global safe list - Introduce global safeCommands() map for command whitelisting. - Implement case-insensitive prefix checking for allowed commands. - Simplify argument handling by removing redundant int conversions. - Update error messages and comments for clarity on security policies. - Remove outdated comments and adjust prompt text for consistency. --- internal/recipe/runner.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index d480722..50db69d 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -258,7 +258,7 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error { return nil } -// executeReadOnlyShell — now handles numbers in args (like tree -L 3) +// executeReadOnlyShell — safe, whitelisted, read-only shell execution with user confirmation func (r *Runner) executeReadOnlyShell(step Step, previousResults []string) { prompt := fmt.Sprintf(`You need additional context from the filesystem for this step. @@ -285,7 +285,7 @@ Return ONLY a JSON array of read-only commands. Example: } ] -Only use safe read-only commands from the allowed list.`, +Only use safe read-only commands.`, r.Recipe.Overview, strings.Join(previousResults, "\n\n---\n\n"), step.Objective, @@ -312,7 +312,7 @@ Only use safe read-only commands from the allowed list.`, type ShellCommand struct { Command string `json:"command"` - Args []interface{} `json:"args"` // allows strings and numbers + Args []interface{} `json:"args"` } var cmds []ShellCommand @@ -321,6 +321,9 @@ Only use safe read-only commands from the allowed list.`, return } + // Use the GLOBAL safe list for the security check + safeMap := safeCommands() + for _, cmd := range cmds { // Build argument list, converting numbers to strings args := make([]string, len(cmd.Args)) @@ -330,8 +333,6 @@ Only use safe read-only commands from the allowed list.`, args[i] = v case float64: args[i] = strconv.FormatFloat(v, 'f', -1, 64) - case int, int64: - args[i] = fmt.Sprintf("%v", v) default: args[i] = fmt.Sprintf("%v", v) } @@ -354,16 +355,18 @@ Only use safe read-only commands from the allowed list.`, continue } - // Whitelist check + // FINAL SECURITY CHECK — use the global safe list allowed := false - for _, allowedCmd := range r.Recipe.AllowedShellCommands { - if strings.HasPrefix(cmd.Command, allowedCmd) { + trimmedCmd := strings.ToLower(strings.TrimSpace(cmd.Command)) + for safe := range safeMap { + if strings.HasPrefix(trimmedCmd, strings.ToLower(safe)) { allowed = true break } } + if !allowed { - fmt.Printf(" ❌ Command not allowed: %s\n", cmd.Command) + fmt.Printf(" ❌ Command not allowed by global safety policy: %s\n", cmd.Command) continue }