Split env handling: FilterByAllowlist at init, MergeEnvVars at exec
- Add FilterByAllowlist: filters os.Environ() by allowlist (run once at init) - Add MergeEnvVars: merges cached env with envSet and extraEnv (no re-filtering) - cachedEnv now stores map[string]string, pre-merged with PICOCLAW_* - PICOCLAW_* vars now survive to child processes
This commit is contained in:
parent
e0ffee8339
commit
7b82cac4df
2 changed files with 85 additions and 5 deletions
|
|
@ -24,7 +24,7 @@ type ExecTool struct {
|
||||||
allowPatterns []*regexp.Regexp
|
allowPatterns []*regexp.Regexp
|
||||||
customAllowPatterns []*regexp.Regexp
|
customAllowPatterns []*regexp.Regexp
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
cachedEnv []string // cached sanitized env from os.Environ() at init
|
cachedEnv map[string]string // cached sanitized env map from os.Environ() at init
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -151,6 +151,15 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
|
||||||
// Ensure PICOCLAW_* vars are set for child processes
|
// Ensure PICOCLAW_* vars are set for child processes
|
||||||
envSet = shell.WithPicoclawEnvVars(envSet, workingDir)
|
envSet = shell.WithPicoclawEnvVars(envSet, workingDir)
|
||||||
|
|
||||||
|
// Build cached env: filter inherited env by allowlist, then merge with config envSet
|
||||||
|
filteredBase := shell.FilterByAllowlist(os.Environ(), envAllowlist)
|
||||||
|
// Pre-merge envSet into cachedEnv so PICOCLAW_* vars are preserved
|
||||||
|
// (MergeEnvVars returns []string, so we merge maps manually)
|
||||||
|
cachedEnv := filteredBase
|
||||||
|
for k, v := range envSet {
|
||||||
|
cachedEnv[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
return &ExecTool{
|
return &ExecTool{
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
|
@ -158,7 +167,7 @@ func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Conf
|
||||||
allowPatterns: nil,
|
allowPatterns: nil,
|
||||||
customAllowPatterns: customAllowPatterns,
|
customAllowPatterns: customAllowPatterns,
|
||||||
restrictToWorkspace: restrict,
|
restrictToWorkspace: restrict,
|
||||||
cachedEnv: shell.BuildSanitizedEnv(os.Environ(), envAllowlist, envSet, nil),
|
cachedEnv: cachedEnv,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,9 +267,9 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
"PICOCLAW_EXEC_TIMEOUT": t.timeout.String(),
|
"PICOCLAW_EXEC_TIMEOUT": t.timeout.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use sanitized environment - strips secrets, prevents env-based attacks
|
// Use sanitized environment - merge cached env with exec time vars and LLM extra env
|
||||||
// Pass extraEnv from LLM to apply blocklist filtering
|
// Note: cachedEnv is NOT re-filtered - PICOCLAW_* vars are preserved
|
||||||
cmd.Env = shell.BuildSanitizedEnv(t.cachedEnv, nil, execTimeEnv, extraEnv)
|
cmd.Env = shell.MergeEnvVars(t.cachedEnv, execTimeEnv, extraEnv)
|
||||||
|
|
||||||
if cwd != "" {
|
if cwd != "" {
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,77 @@ var windowsEnvAllowlist = map[string]bool{
|
||||||
"HOMEPATH": true,
|
"HOMEPATH": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterByAllowlist filters the inherited environment to only allowlisted variables.
|
||||||
|
// This should only be used at init time to create the cached environment.
|
||||||
|
func FilterByAllowlist(baseEnv []string, extraAllowlist []string) map[string]string {
|
||||||
|
if baseEnv == nil {
|
||||||
|
baseEnv = os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed := make(map[string]bool, len(DefaultEnvAllowlist)+len(extraAllowlist)+len(windowsEnvAllowlist))
|
||||||
|
for k := range DefaultEnvAllowlist {
|
||||||
|
allowed[envKey(k)] = true
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
for k := range windowsEnvAllowlist {
|
||||||
|
allowed[envKey(k)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range extraAllowlist {
|
||||||
|
allowed[envKey(k)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := make(map[string]string)
|
||||||
|
for _, entry := range baseEnv {
|
||||||
|
k, v, ok := strings.Cut(entry, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
norm := envKey(k)
|
||||||
|
if allowed[norm] || isAllowedPrefix(norm) {
|
||||||
|
vars[norm] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeEnvVars merges multiple env sources into a final []string for exec.Cmd.Env.
|
||||||
|
// baseEnv is NOT filtered - it's assumed to already be sanitized (e.g., cachedEnv).
|
||||||
|
// envSet provides explicit key=value pairs (config, not filtered).
|
||||||
|
// extraEnv provides additional key=value pairs from LLM (filtered by blocklist).
|
||||||
|
func MergeEnvVars(baseEnv map[string]string, envSet, extraEnv map[string]string) []string {
|
||||||
|
vars := make(map[string]string, len(baseEnv)+len(envSet)+len(extraEnv))
|
||||||
|
|
||||||
|
// Start with base env (already sanitized)
|
||||||
|
for k, v := range baseEnv {
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add envSet (config-provided, not filtered)
|
||||||
|
if envSet != nil {
|
||||||
|
for k, v := range envSet {
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge extraEnv (LLM-provided) - filtered by blocklist
|
||||||
|
if extraEnv != nil {
|
||||||
|
for k, v := range extraEnv {
|
||||||
|
if LLMBlocklist[envKey(k)] {
|
||||||
|
continue // Skip blocked vars
|
||||||
|
}
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to []string for exec.Cmd.Env
|
||||||
|
result := make([]string, 0, len(vars))
|
||||||
|
for k, v := range vars {
|
||||||
|
result = append(result, k+"="+v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// BuildSanitizedEnv constructs a sanitized environment []string suitable for
|
// BuildSanitizedEnv constructs a sanitized environment []string suitable for
|
||||||
// exec.Cmd.Env. It filters the inherited environment to only allowlisted variables.
|
// exec.Cmd.Env. It filters the inherited environment to only allowlisted variables.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue