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.
This commit is contained in:
Greg Gauthier 2026-03-07 19:50:06 +00:00
parent 4603a1ec7a
commit 383d28a91a

View File

@ -258,7 +258,7 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error {
return nil 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) { func (r *Runner) executeReadOnlyShell(step Step, previousResults []string) {
prompt := fmt.Sprintf(`You need additional context from the filesystem for this step. 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, r.Recipe.Overview,
strings.Join(previousResults, "\n\n---\n\n"), strings.Join(previousResults, "\n\n---\n\n"),
step.Objective, step.Objective,
@ -312,7 +312,7 @@ Only use safe read-only commands from the allowed list.`,
type ShellCommand struct { type ShellCommand struct {
Command string `json:"command"` Command string `json:"command"`
Args []interface{} `json:"args"` // allows strings and numbers Args []interface{} `json:"args"`
} }
var cmds []ShellCommand var cmds []ShellCommand
@ -321,6 +321,9 @@ Only use safe read-only commands from the allowed list.`,
return return
} }
// Use the GLOBAL safe list for the security check
safeMap := safeCommands()
for _, cmd := range cmds { for _, cmd := range cmds {
// Build argument list, converting numbers to strings // Build argument list, converting numbers to strings
args := make([]string, len(cmd.Args)) args := make([]string, len(cmd.Args))
@ -330,8 +333,6 @@ Only use safe read-only commands from the allowed list.`,
args[i] = v args[i] = v
case float64: case float64:
args[i] = strconv.FormatFloat(v, 'f', -1, 64) args[i] = strconv.FormatFloat(v, 'f', -1, 64)
case int, int64:
args[i] = fmt.Sprintf("%v", v)
default: default:
args[i] = fmt.Sprintf("%v", v) args[i] = fmt.Sprintf("%v", v)
} }
@ -354,16 +355,18 @@ Only use safe read-only commands from the allowed list.`,
continue continue
} }
// Whitelist check // FINAL SECURITY CHECK — use the global safe list
allowed := false allowed := false
for _, allowedCmd := range r.Recipe.AllowedShellCommands { trimmedCmd := strings.ToLower(strings.TrimSpace(cmd.Command))
if strings.HasPrefix(cmd.Command, allowedCmd) { for safe := range safeMap {
if strings.HasPrefix(trimmedCmd, strings.ToLower(safe)) {
allowed = true allowed = true
break break
} }
} }
if !allowed { 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 continue
} }