diff --git a/internal/recipe/loader.go b/internal/recipe/loader.go index b4ac37d..5a890ec 100644 --- a/internal/recipe/loader.go +++ b/internal/recipe/loader.go @@ -11,6 +11,21 @@ import ( "gopkg.in/yaml.v3" ) +// Global safe read-only whitelist +var safeReadOnlyCommands = map[string]bool{ + "ls": true, + "pwd": true, + "cat": true, + "tree": true, + "git status": true, + "git log": true, + "find": true, + "grep": true, + "cndump -s": true, + "tea repos list -o csv -lm 100": true, + "tea repos search -o csv": true, +} + var ( // stepRe still finds the headings (this one is solid) stepRe = regexp.MustCompile(`(?m)^### Step (\d+): (.+)$`) @@ -32,6 +47,14 @@ func Load(path string, userParams map[string]any) (*Recipe, error) { return nil, fmt.Errorf("yaml parse: %w", err) } + // === SAFETY CHECK: reject dangerous allowed_shell_commands === + for _, cmd := range r.AllowedShellCommands { + trimmed := strings.TrimSpace(strings.ToLower(cmd)) + if !safeReadOnlyCommands[trimmed] && !strings.HasPrefix(trimmed, "git status") && !strings.HasPrefix(trimmed, "git log") { + return nil, fmt.Errorf("\033[31mERROR: Recipe contains unsafe shell command: %q\033[0m\n\nOnly the following read-only commands are allowed:\n ls, pwd, cat, tree, git status, git log, find, grep\n\nRemove or replace the dangerous command and try again.", cmd) + } + } + // Apply defaults + user --param overrides if r.Parameters == nil { r.Parameters = make(map[string]Parameter)