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"`
|
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"`
|
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)
|
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 {
|
type SkillsToolsConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type ExecTool struct {
|
||||||
allowPatterns []*regexp.Regexp
|
allowPatterns []*regexp.Regexp
|
||||||
customAllowPatterns []*regexp.Regexp
|
customAllowPatterns []*regexp.Regexp
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
|
filterEnv bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -136,6 +137,11 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
|
||||||
timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second
|
timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterEnv := false
|
||||||
|
if config != nil {
|
||||||
|
filterEnv = config.Tools.Exec.FilterEnv
|
||||||
|
}
|
||||||
|
|
||||||
return &ExecTool{
|
return &ExecTool{
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
|
@ -143,6 +149,7 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
|
||||||
allowPatterns: nil,
|
allowPatterns: nil,
|
||||||
customAllowPatterns: customAllowPatterns,
|
customAllowPatterns: customAllowPatterns,
|
||||||
restrictToWorkspace: restrict,
|
restrictToWorkspace: restrict,
|
||||||
|
filterEnv: filterEnv,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,6 +228,10 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.filterEnv {
|
||||||
|
cmd.Env = filterEnvironment(os.Environ())
|
||||||
|
}
|
||||||
|
|
||||||
prepareCommandForTermination(cmd)
|
prepareCommandForTermination(cmd)
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
@ -381,3 +392,38 @@ func (t *ExecTool) SetAllowPatterns(patterns []string) error {
|
||||||
}
|
}
|
||||||
return nil
|
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