From 6ab2d7034886f71765c96f95fe00e72a62375610 Mon Sep 17 00:00:00 2001 From: admin-mf Date: Fri, 6 Mar 2026 00:13:32 -0600 Subject: [PATCH] security(shell): add opt-in environment variable filtering When filter_env is enabled in config, shell commands run with a minimal allowlist of environment variables (PATH, HOME, LANG, TERM, etc. plus PICOCLAW_* prefix), preventing accidental leakage of sensitive env vars like API keys to spawned processes. Disabled by default for backward compat. Co-Authored-By: Claude Opus 4.6 --- pkg/config/config.go | 1 + pkg/tools/shell.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/pkg/config/config.go b/pkg/config/config.go index d5c280e18..2e1ec22e0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -618,6 +618,7 @@ type ExecConfig struct { CustomDenyPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS" json:"custom_deny_patterns"` CustomAllowPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS" json:"custom_allow_patterns"` TimeoutSeconds int ` env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s) + FilterEnv bool ` env:"PICOCLAW_TOOLS_EXEC_FILTER_ENV" json:"filter_env"` } type SkillsToolsConfig struct { diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index b8a811d03..9204ccf3a 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -23,6 +23,7 @@ type ExecTool struct { allowPatterns []*regexp.Regexp customAllowPatterns []*regexp.Regexp restrictToWorkspace bool + filterEnv bool } var ( @@ -136,6 +137,11 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second } + filterEnv := false + if config != nil { + filterEnv = config.Tools.Exec.FilterEnv + } + return &ExecTool{ workingDir: workingDir, timeout: timeout, @@ -143,6 +149,7 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf allowPatterns: nil, customAllowPatterns: customAllowPatterns, restrictToWorkspace: restrict, + filterEnv: filterEnv, }, nil } @@ -221,6 +228,10 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult cmd.Dir = cwd } + if t.filterEnv { + cmd.Env = filterEnvironment(os.Environ()) + } + prepareCommandForTermination(cmd) var stdout, stderr bytes.Buffer @@ -381,3 +392,38 @@ func (t *ExecTool) SetAllowPatterns(patterns []string) error { } return nil } + +// safeEnvVars is the allowlist of environment variable names preserved when +// filterEnv is enabled. Variables with a PICOCLAW_ prefix are always kept. +var safeEnvVars = map[string]bool{ + "PATH": true, + "HOME": true, + "USER": true, + "LANG": true, + "TERM": true, + "SHELL": true, + "TMPDIR": true, + "TMP": true, + "TEMP": true, + "SystemRoot": true, + "COMSPEC": true, + "USERPROFILE": true, + "APPDATA": true, + "LOCALAPPDATA": true, +} + +// filterEnvironment returns a filtered copy of environ keeping only safe vars. +func filterEnvironment(environ []string) []string { + filtered := make([]string, 0, len(safeEnvVars)+4) + for _, entry := range environ { + eqIdx := strings.IndexByte(entry, '=') + if eqIdx < 0 { + continue + } + name := entry[:eqIdx] + if safeEnvVars[name] || strings.HasPrefix(name, "PICOCLAW_") { + filtered = append(filtered, entry) + } + } + return filtered +}