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 <noreply@anthropic.com>
This commit is contained in:
parent
503bc3f1d2
commit
6ab2d70348
2 changed files with 47 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue