feat(security): add environment sanitization for shell execution
- Add BuildSanitizedEnv() with default allowlist (PATH, HOME, USER, etc) - Add PICOCLAW_* vars to allowlist for config propagation - Add LLMBlocklist for vars LLM cannot override - Cache sanitized env at ExecTool init, pass to exec.Cmd - Add tests and docs Part of PR1: Environment Sanitization for External Execution
This commit is contained in:
parent
9cfb1b5795
commit
b4e18abcb8
5 changed files with 439 additions and 3 deletions
|
|
@ -52,10 +52,35 @@ The exec tool is used to execute shell commands.
|
||||||
| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
|
| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
|
||||||
| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
|
| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
|
||||||
|
|
||||||
### Functionality
|
### Environment Sanitization
|
||||||
|
|
||||||
- **`enable_deny_patterns`**: Set to `false` to completely disable the default dangerous command blocking patterns
|
The exec tool sanitizes the environment passed to child processes:
|
||||||
- **`custom_deny_patterns`**: Add custom deny regex patterns; commands matching these will be blocked
|
|
||||||
|
1. **Default allowlist** — Only these variables are inherited from the parent process:
|
||||||
|
- `PATH`, `HOME`, `USER`, `LANG`, `SHELL`, `TERM`, `PWD`, `OLDPWD`, `HOSTNAME`, `LOGNAME`, `TZ`, `DISPLAY`, `TMPDIR`, `EDITOR`, `PAGER`
|
||||||
|
- Plus: `PICOCLAW_CONFIG`, `PICOCLAW_HOME`, `PICOCLAW_SERVICE_NAME`, `PICOCLAW_EXE`, `PICOCLAW_WORKSPACE`
|
||||||
|
- Plus: `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`
|
||||||
|
- Plus any variable starting with `LC_`
|
||||||
|
|
||||||
|
2. **Config env_set** — Variables from config are merged (can override inherited values)
|
||||||
|
|
||||||
|
3. **LLM env injection** — The LLM can inject additional variables per-call via the `env` parameter:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "exec",
|
||||||
|
"arguments": {
|
||||||
|
"command": "echo $DEBUG_MODE",
|
||||||
|
"env": {
|
||||||
|
"DEBUG_MODE": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Blocked variables** — The LLM cannot override these sensitive variables:
|
||||||
|
- `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`
|
||||||
|
- `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `LD_DEBUG`
|
||||||
|
|
||||||
### Default Blocked Command Patterns
|
### Default Blocked Command Patterns
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/tools/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExecTool struct {
|
type ExecTool struct {
|
||||||
|
|
@ -23,6 +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
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -143,6 +145,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(), nil, nil, nil),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +220,10 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
} else {
|
} else {
|
||||||
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
|
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use sanitized environment - strips secrets, prevents env-based attacks
|
||||||
|
cmd.Env = t.cachedEnv
|
||||||
|
|
||||||
if cwd != "" {
|
if cwd != "" {
|
||||||
cmd.Dir = cwd
|
cmd.Dir = cwd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
156
pkg/tools/shell/env.go
Normal file
156
pkg/tools/shell/env.go
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultEnvAllowlist is the set of environment variable names that are safe
|
||||||
|
// to propagate to child processes. Everything else is stripped to prevent
|
||||||
|
// accidental credential leakage.
|
||||||
|
//
|
||||||
|
// To add a new variable:
|
||||||
|
// 1. Add to this map if it's safe to pass through
|
||||||
|
// 2. Or add a prefix to defaultEnvAllowPrefixes for pattern matching
|
||||||
|
// Note: Do NOT add wildcard patterns like "*_API_KEY" here - use explicit names
|
||||||
|
// to avoid accidentally leaking secrets.
|
||||||
|
var DefaultEnvAllowlist = map[string]bool{
|
||||||
|
"PATH": true,
|
||||||
|
"HOME": true,
|
||||||
|
"USER": true,
|
||||||
|
"LANG": true,
|
||||||
|
"SHELL": true,
|
||||||
|
"TERM": true,
|
||||||
|
"PWD": true,
|
||||||
|
"OLDPWD": true,
|
||||||
|
"HOSTNAME": true,
|
||||||
|
"LOGNAME": true,
|
||||||
|
"TZ": true,
|
||||||
|
"DISPLAY": true,
|
||||||
|
"TMPDIR": true,
|
||||||
|
"EDITOR": true,
|
||||||
|
"PAGER": true,
|
||||||
|
"HTTP_PROXY": true,
|
||||||
|
"HTTPS_PROXY": true,
|
||||||
|
"NO_PROXY": true,
|
||||||
|
// PICOCLAW_* vars - inherited from parent process if set
|
||||||
|
"PICOCLAW_HOME": true,
|
||||||
|
"PICOCLAW_CONFIG": true,
|
||||||
|
"PICOCLAW_SERVICE_NAME": true,
|
||||||
|
"PICOCLAW_EXE": true,
|
||||||
|
"PICOCLAW_WORKSPACE": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultEnvAllowPrefixes are env var prefixes that are always allowed.
|
||||||
|
var defaultEnvAllowPrefixes = []string{
|
||||||
|
"LC_",
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBlocklist is the set of environment variable names that the LLM
|
||||||
|
// cannot override, even if passed via the env parameter. These vars
|
||||||
|
// control fundamental process behavior and could be exploited.
|
||||||
|
var LLMBlocklist = map[string]bool{
|
||||||
|
"PATH": true, // Could hijack command resolution
|
||||||
|
"HOME": true, // Could redirect file access
|
||||||
|
"USER": true, // Could impersonate user
|
||||||
|
"LOGNAME": true, // Could impersonate user
|
||||||
|
"SHELL": true, // Could change shell behavior
|
||||||
|
"LD_PRELOAD": true, // Could inject code
|
||||||
|
"LD_LIBRARY_PATH": true, // Could hijack library resolution
|
||||||
|
"LD_AUDIT": true, // Could inject code
|
||||||
|
"LD_DEBUG": true, // Could leak info
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsEnvAllowlist contains additional variables needed on Windows.
|
||||||
|
var windowsEnvAllowlist = map[string]bool{
|
||||||
|
"PATHEXT": true,
|
||||||
|
"SYSTEMROOT": true,
|
||||||
|
"SYSTEMDRIVE": true,
|
||||||
|
"COMSPEC": true,
|
||||||
|
"APPDATA": true,
|
||||||
|
"USERPROFILE": true,
|
||||||
|
"HOMEDRIVE": true,
|
||||||
|
"HOMEPATH": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSanitizedEnv constructs a sanitized environment []string suitable for
|
||||||
|
// exec.Cmd.Env. It filters the inherited environment to only allowlisted variables.
|
||||||
|
//
|
||||||
|
// baseEnv is the inherited environment (e.g., from os.Environ() or cached).
|
||||||
|
// If nil, os.Environ() will be used for backwards compatibility.
|
||||||
|
// extraAllowlist adds additional variable names to the default allowlist.
|
||||||
|
// envSet provides explicit key=value pairs from config (override inherited).
|
||||||
|
// extraEnv provides additional key=value pairs from tool call (merged with envSet).
|
||||||
|
func BuildSanitizedEnv(baseEnv []string, extraAllowlist []string, envSet, extraEnv map[string]string) []string {
|
||||||
|
// Use provided env or fall back to os.Environ
|
||||||
|
inherited := baseEnv
|
||||||
|
if inherited == nil {
|
||||||
|
inherited = 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, len(allowed)+len(envSet)+len(extraEnv))
|
||||||
|
|
||||||
|
for _, entry := range inherited {
|
||||||
|
k, v, ok := strings.Cut(entry, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
norm := envKey(k)
|
||||||
|
if allowed[norm] || isAllowedPrefix(norm) {
|
||||||
|
vars[norm] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range envSet {
|
||||||
|
vars[envKey(k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge extraEnv (tool call) - highest priority
|
||||||
|
// Filter against LLM blocklist to prevent override of sensitive vars
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// envKey normalizes an environment variable name. On Windows, where env
|
||||||
|
// vars are case-insensitive, it uppercases the key so that "Path" and
|
||||||
|
// "PATH" map to the same entry. On other platforms it's a no-op.
|
||||||
|
func envKey(k string) string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedPrefix(name string) bool {
|
||||||
|
for _, prefix := range defaultEnvAllowPrefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
180
pkg/tools/shell/env_test.go
Normal file
180
pkg/tools/shell/env_test.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_FiltersSecrets(t *testing.T) {
|
||||||
|
// Set some secret env vars.
|
||||||
|
t.Setenv("OPENAI_API_KEY", "sk-secret")
|
||||||
|
t.Setenv("ANTHROPIC_API_KEY", "anthro-secret")
|
||||||
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "aws-secret")
|
||||||
|
t.Setenv("DATABASE_URL", "postgres://secret")
|
||||||
|
// Set something that should pass.
|
||||||
|
t.Setenv("PATH", "/usr/bin")
|
||||||
|
t.Setenv("HOME", "/home/test")
|
||||||
|
t.Setenv("LC_ALL", "en_US.UTF-8")
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assertEnvPresent(t, env, "PATH")
|
||||||
|
assertEnvPresent(t, env, "HOME")
|
||||||
|
assertEnvPresent(t, env, "LC_ALL")
|
||||||
|
|
||||||
|
assertEnvAbsent(t, env, "OPENAI_API_KEY")
|
||||||
|
assertEnvAbsent(t, env, "ANTHROPIC_API_KEY")
|
||||||
|
assertEnvAbsent(t, env, "AWS_SECRET_ACCESS_KEY")
|
||||||
|
assertEnvAbsent(t, env, "DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_ExtraAllowlist(t *testing.T) {
|
||||||
|
t.Setenv("MY_CUSTOM_VAR", "hello")
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, []string{"MY_CUSTOM_VAR"}, nil, nil)
|
||||||
|
assertEnvPresent(t, env, "MY_CUSTOM_VAR")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_EnvSet(t *testing.T) {
|
||||||
|
env := BuildSanitizedEnv(nil, nil, map[string]string{
|
||||||
|
"INJECTED": "value123",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
v := getEnvValue(env, "INJECTED")
|
||||||
|
if v == "" {
|
||||||
|
t.Fatal("expected INJECTED to be present")
|
||||||
|
}
|
||||||
|
if v != "value123" {
|
||||||
|
t.Errorf("INJECTED = %q, want %q", v, "value123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_EnvSetOverridesInherited(t *testing.T) {
|
||||||
|
t.Setenv("PATH", "/original")
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, map[string]string{
|
||||||
|
"PATH": "/overridden",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
v := getEnvValue(env, "PATH")
|
||||||
|
if v != "/overridden" {
|
||||||
|
t.Errorf("PATH = %q, want %q", v, "/overridden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_DefaultAllowlist(t *testing.T) {
|
||||||
|
for name := range DefaultEnvAllowlist {
|
||||||
|
t.Setenv(name, "test-"+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, nil, nil)
|
||||||
|
|
||||||
|
for name := range DefaultEnvAllowlist {
|
||||||
|
v := getEnvValue(env, name)
|
||||||
|
if v == "" {
|
||||||
|
t.Errorf("expected %s to be in sanitized env", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_ReturnsSlice(t *testing.T) {
|
||||||
|
env := BuildSanitizedEnv(nil, nil, map[string]string{
|
||||||
|
"TEST_A": "a",
|
||||||
|
"TEST_B": "b",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if env == nil {
|
||||||
|
t.Fatal("env should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be a slice
|
||||||
|
if len(env) == 0 {
|
||||||
|
t.Error("expected non-empty env slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check format
|
||||||
|
found := make(map[string]string)
|
||||||
|
for _, entry := range env {
|
||||||
|
k, v, ok := strings.Cut(entry, "=")
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid env entry: %q", entry)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if found["TEST_A"] != "a" {
|
||||||
|
t.Errorf("TEST_A = %q, want %q", found["TEST_A"], "a")
|
||||||
|
}
|
||||||
|
if found["TEST_B"] != "b" {
|
||||||
|
t.Errorf("TEST_B = %q, want %q", found["TEST_B"], "b")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLLMBlocklist_BlocksSensitiveVars(t *testing.T) {
|
||||||
|
// Set up inherited env
|
||||||
|
t.Setenv("PATH", "/original/path")
|
||||||
|
t.Setenv("HOME", "/original/home")
|
||||||
|
|
||||||
|
// LLM tries to override these via extraEnv
|
||||||
|
extraEnv := map[string]string{
|
||||||
|
"PATH": "/malicious/path",
|
||||||
|
"HOME": "/etc",
|
||||||
|
"LD_PRELOAD": "/evil.so",
|
||||||
|
"MY_CUSTOM_VAR": "allowed", // Not blocked
|
||||||
|
}
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, nil, extraEnv)
|
||||||
|
|
||||||
|
// Blocked vars should keep their inherited value, not the LLM override
|
||||||
|
v := getEnvValue(env, "PATH")
|
||||||
|
if v != "/original/path" {
|
||||||
|
t.Errorf("PATH should be /original/path, got %q - LLM override was not blocked", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = getEnvValue(env, "HOME")
|
||||||
|
if v != "/original/home" {
|
||||||
|
t.Errorf("HOME should be /original/home, got %q - LLM override was not blocked", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LD_PRELOAD was not in inherited, so should still be absent (blocked)
|
||||||
|
v = getEnvValue(env, "LD_PRELOAD")
|
||||||
|
if v != "" {
|
||||||
|
t.Error("LD_PRELOAD should be blocked by LLMBlocklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-blocked var should be present
|
||||||
|
v = getEnvValue(env, "MY_CUSTOM_VAR")
|
||||||
|
if v == "" {
|
||||||
|
t.Error("MY_CUSTOM_VAR should be allowed")
|
||||||
|
}
|
||||||
|
if v != "allowed" {
|
||||||
|
t.Errorf("MY_CUSTOM_VAR = %q, want %q", v, "allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get value from env slice
|
||||||
|
func getEnvValue(env []string, name string) string {
|
||||||
|
prefix := name + "="
|
||||||
|
for _, entry := range env {
|
||||||
|
if strings.HasPrefix(entry, prefix) {
|
||||||
|
return strings.TrimPrefix(entry, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEnvPresent(t *testing.T, env []string, name string) {
|
||||||
|
t.Helper()
|
||||||
|
if getEnvValue(env, name) == "" {
|
||||||
|
t.Errorf("expected %s to be present in sanitized env", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEnvAbsent(t *testing.T, env []string, name string) {
|
||||||
|
t.Helper()
|
||||||
|
v := getEnvValue(env, name)
|
||||||
|
if v != "" {
|
||||||
|
t.Errorf("expected %s to be absent from sanitized env, got %q", name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
pkg/tools/shell/env_windows_test.go
Normal file
68
pkg/tools/shell/env_windows_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_WindowsCaseInsensitive(t *testing.T) {
|
||||||
|
// Skip on non-Windows
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
t.Skip("Windows-specific test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows the OS typically stores "Path" not "PATH".
|
||||||
|
// Verify that mixed-case inherited vars still pass the allowlist.
|
||||||
|
t.Setenv("Path", `C:\Windows\system32`)
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, nil, nil)
|
||||||
|
|
||||||
|
v := getEnvValue(env, "PATH")
|
||||||
|
if v == "" {
|
||||||
|
t.Fatal("expected PATH to be present when OS provides 'Path'")
|
||||||
|
}
|
||||||
|
if v != `C:\Windows\system32` {
|
||||||
|
t.Errorf("PATH = %q, want %q", v, `C:\Windows\system32`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also verify lookup with original casing works.
|
||||||
|
v2 := getEnvValue(env, "Path")
|
||||||
|
if v2 == "" {
|
||||||
|
t.Fatal("expected Get('Path') to resolve via case-insensitive lookup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_WindowsEnvSetCaseInsensitive(t *testing.T) {
|
||||||
|
// Skip on non-Windows
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
t.Skip("Windows-specific test")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, nil, map[string]string{
|
||||||
|
"path": `C:\custom\bin`,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
v := getEnvValue(env, "PATH")
|
||||||
|
if v == "" {
|
||||||
|
t.Fatal("expected PATH to be set via lowercase 'path' envSet key")
|
||||||
|
}
|
||||||
|
if v != `C:\custom\bin` {
|
||||||
|
t.Errorf("PATH = %q, want %q", v, `C:\custom\bin`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSanitizedEnv_WindowsExtraAllowlistCaseInsensitive(t *testing.T) {
|
||||||
|
// Skip on non-Windows
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
t.Skip("Windows-specific test")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("my_custom_var", "hello")
|
||||||
|
|
||||||
|
env := BuildSanitizedEnv(nil, []string{"MY_CUSTOM_VAR"}, nil, nil)
|
||||||
|
|
||||||
|
v := getEnvValue(env, "my_custom_var")
|
||||||
|
if v == "" {
|
||||||
|
t.Fatal("expected my_custom_var to be found via case-insensitive allowlist")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue