Add environment sanitization
This commit is contained in:
@@ -1,23 +1,28 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
Fence restricts network and filesystem access for arbitrary commands. It works by:
|
Fence restricts network, filesystem, and command access for arbitrary commands. It works by:
|
||||||
|
|
||||||
1. **Intercepting network traffic** via HTTP/SOCKS5 proxies that filter by domain
|
1. **Blocking commands** via configurable deny/allow lists before execution
|
||||||
2. **Sandboxing processes** using OS-native mechanisms (macOS sandbox-exec, Linux bubblewrap)
|
2. **Intercepting network traffic** via HTTP/SOCKS5 proxies that filter by domain
|
||||||
3. **Bridging connections** to allow controlled inbound/outbound traffic in isolated namespaces
|
3. **Sandboxing processes** using OS-native mechanisms (macOS sandbox-exec, Linux bubblewrap)
|
||||||
|
4. **Sanitizing environment** by stripping dangerous variables (LD_PRELOAD, DYLD_INSERT_LIBRARIES, etc.)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph Fence
|
subgraph Fence
|
||||||
Config["Config<br/>(JSON)"]
|
Config["Config<br/>(JSON)"]
|
||||||
Manager
|
Manager
|
||||||
|
CmdCheck["Command<br/>Blocking"]
|
||||||
|
EnvSanitize["Env<br/>Sanitization"]
|
||||||
Sandbox["Platform Sandbox<br/>(macOS/Linux)"]
|
Sandbox["Platform Sandbox<br/>(macOS/Linux)"]
|
||||||
HTTP["HTTP Proxy<br/>(filtering)"]
|
HTTP["HTTP Proxy<br/>(filtering)"]
|
||||||
SOCKS["SOCKS5 Proxy<br/>(filtering)"]
|
SOCKS["SOCKS5 Proxy<br/>(filtering)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
Config --> Manager
|
Config --> Manager
|
||||||
Manager --> Sandbox
|
Manager --> CmdCheck
|
||||||
|
CmdCheck --> EnvSanitize
|
||||||
|
EnvSanitize --> Sandbox
|
||||||
Manager --> HTTP
|
Manager --> HTTP
|
||||||
Manager --> SOCKS
|
Manager --> SOCKS
|
||||||
```
|
```
|
||||||
@@ -42,6 +47,8 @@ fence/
|
|||||||
│ ├── linux_features.go # Kernel feature detection
|
│ ├── linux_features.go # Kernel feature detection
|
||||||
│ ├── linux_*_stub.go # Non-Linux build stubs
|
│ ├── linux_*_stub.go # Non-Linux build stubs
|
||||||
│ ├── monitor.go # macOS log stream violation monitoring
|
│ ├── monitor.go # macOS log stream violation monitoring
|
||||||
|
│ ├── command.go # Command blocking/allow lists
|
||||||
|
│ ├── hardening.go # Environment sanitization
|
||||||
│ ├── dangerous.go # Protected file/directory lists
|
│ ├── dangerous.go # Protected file/directory lists
|
||||||
│ ├── shell.go # Shell quoting utilities
|
│ ├── shell.go # Shell quoting utilities
|
||||||
│ └── utils.go # Path normalization
|
│ └── utils.go # Path normalization
|
||||||
@@ -59,12 +66,13 @@ Handles loading and validating sandbox configuration:
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Network NetworkConfig // Domain allow/deny lists
|
Network NetworkConfig // Domain allow/deny lists
|
||||||
Filesystem FilesystemConfig // Read/write restrictions
|
Filesystem FilesystemConfig // Read/write restrictions
|
||||||
|
Command CommandConfig // Command deny/allow lists
|
||||||
AllowPty bool // Allow pseudo-terminal allocation
|
AllowPty bool // Allow pseudo-terminal allocation
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Loads from `~/.fence.json` or custom path
|
- Loads from `~/.fence.json` or custom path
|
||||||
- Falls back to restrictive defaults (block all network)
|
- Falls back to restrictive defaults (block all network, default command deny list)
|
||||||
- Validates paths and normalizes them
|
- Validates paths and normalizes them
|
||||||
|
|
||||||
### Platform (`internal/platform/`)
|
### Platform (`internal/platform/`)
|
||||||
@@ -108,8 +116,27 @@ Orchestrates the sandbox lifecycle:
|
|||||||
|
|
||||||
1. Initializes HTTP and SOCKS proxies
|
1. Initializes HTTP and SOCKS proxies
|
||||||
2. Sets up platform-specific bridges (Linux)
|
2. Sets up platform-specific bridges (Linux)
|
||||||
3. Wraps commands with sandbox restrictions
|
3. Checks command against deny/allow lists
|
||||||
4. Handles cleanup on exit
|
4. Wraps commands with sandbox restrictions
|
||||||
|
5. Handles cleanup on exit
|
||||||
|
|
||||||
|
#### Command Blocking (`command.go`)
|
||||||
|
|
||||||
|
Blocks commands before they run based on configurable policies:
|
||||||
|
|
||||||
|
- **Default deny list**: Dangerous system commands (`shutdown`, `reboot`, `mkfs`, `rm -rf`, etc.)
|
||||||
|
- **Custom deny/allow**: User-configured prefixes (e.g., `git push`, `npm publish`)
|
||||||
|
- **Chain detection**: Parses `&&`, `||`, `;`, `|` to catch blocked commands in pipelines
|
||||||
|
- **Nested shells**: Detects `bash -c "blocked_cmd"` patterns
|
||||||
|
|
||||||
|
#### Environment Sanitization (`hardening.go`)
|
||||||
|
|
||||||
|
Strips dangerous environment variables before command execution:
|
||||||
|
|
||||||
|
- Linux: `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, etc.
|
||||||
|
- macOS: `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, etc.
|
||||||
|
|
||||||
|
This prevents library injection attacks where a sandboxed process writes a malicious `.so`/`.dylib` and uses `LD_PRELOAD`/`DYLD_INSERT_LIBRARIES` in a subsequent command.
|
||||||
|
|
||||||
#### macOS Implementation (`macos.go`)
|
#### macOS Implementation (`macos.go`)
|
||||||
|
|
||||||
@@ -229,15 +256,18 @@ flowchart TD
|
|||||||
|
|
||||||
D1 & D2 & D3 & D4 --> E["5. Manager.WrapCommand()"]
|
D1 & D2 & D3 & D4 --> E["5. Manager.WrapCommand()"]
|
||||||
|
|
||||||
E --> E1["[macOS] Generate Seatbelt profile"]
|
E --> E0{"Check command<br/>deny/allow lists"}
|
||||||
E --> E2["[Linux] Generate bwrap command"]
|
E0 -->|blocked| ERR["Return error"]
|
||||||
|
E0 -->|allowed| E1["[macOS] Generate Seatbelt profile"]
|
||||||
|
E0 -->|allowed| E2["[Linux] Generate bwrap command"]
|
||||||
|
|
||||||
E1 & E2 --> F["6. Execute wrapped command"]
|
E1 & E2 --> F["6. Sanitize env<br/>(strip LD_*/DYLD_*)"]
|
||||||
F --> G["7. Manager.Cleanup()"]
|
F --> G["7. Execute wrapped command"]
|
||||||
|
G --> H["8. Manager.Cleanup()"]
|
||||||
|
|
||||||
G --> G1["Kill socat processes"]
|
H --> H1["Kill socat processes"]
|
||||||
G --> G2["Remove Unix sockets"]
|
H --> H2["Remove Unix sockets"]
|
||||||
G --> G3["Stop proxy servers"]
|
H --> H3["Stop proxy servers"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Platform Comparison
|
## Platform Comparison
|
||||||
@@ -251,6 +281,7 @@ flowchart TD
|
|||||||
| Syscall filtering | Implicit (Seatbelt) | seccomp BPF |
|
| Syscall filtering | Implicit (Seatbelt) | seccomp BPF |
|
||||||
| Inbound connections | Profile rules (`network-bind`) | Reverse socat bridges |
|
| Inbound connections | Profile rules (`network-bind`) | Reverse socat bridges |
|
||||||
| Violation monitoring | log stream + proxy | eBPF + proxy |
|
| Violation monitoring | log stream + proxy | eBPF + proxy |
|
||||||
|
| Env sanitization | Strips DYLD_* | Strips LD_* |
|
||||||
| Requirements | Built-in | bwrap, socat |
|
| Requirements | Built-in | bwrap, socat |
|
||||||
|
|
||||||
### Linux Security Layers
|
### Linux Security Layers
|
||||||
@@ -269,7 +300,7 @@ See [Linux Security Features](./docs/linux-security-features.md) for details.
|
|||||||
|
|
||||||
## Violation Monitoring
|
## Violation Monitoring
|
||||||
|
|
||||||
The `-m` (monitor) flag enables real-time visibility into blocked operations.
|
The `-m` (monitor) flag enables real-time visibility into blocked operations. These only apply to filesystem and network operations, not blocked commands.
|
||||||
|
|
||||||
### Output Prefixes
|
### Output Prefixes
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,15 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Fprintf(os.Stderr, "[fence] Sandboxed command: %s\n", sandboxedCommand)
|
fmt.Fprintf(os.Stderr, "[fence] Sandboxed command: %s\n", sandboxedCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hardenedEnv := sandbox.GetHardenedEnv()
|
||||||
|
if debug {
|
||||||
|
if stripped := sandbox.GetStrippedEnvVars(os.Environ()); len(stripped) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "[fence] Stripped dangerous env vars: %v\n", stripped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
execCmd := exec.Command("sh", "-c", sandboxedCommand) //nolint:gosec // sandboxedCommand is constructed from user input - intentional
|
execCmd := exec.Command("sh", "-c", sandboxedCommand) //nolint:gosec // sandboxedCommand is constructed from user input - intentional
|
||||||
|
execCmd.Env = hardenedEnv
|
||||||
execCmd.Stdin = os.Stdin
|
execCmd.Stdin = os.Stdin
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
execCmd.Stderr = os.Stderr
|
execCmd.Stderr = os.Stderr
|
||||||
@@ -318,8 +326,11 @@ parseCommand:
|
|||||||
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec: %s %v\n", execPath, command[1:])
|
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec: %s %v\n", execPath, command[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize environment (strips LD_PRELOAD, etc.)
|
||||||
|
hardenedEnv := sandbox.FilterDangerousEnv(os.Environ())
|
||||||
|
|
||||||
// Exec the command (replaces this process)
|
// Exec the command (replaces this process)
|
||||||
err = syscall.Exec(execPath, command, os.Environ()) //nolint:gosec
|
err = syscall.Exec(execPath, command, hardenedEnv) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec failed: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ Localhost is separate from "external domains":
|
|||||||
- **denyRead** can block reads from sensitive paths.
|
- **denyRead** can block reads from sensitive paths.
|
||||||
- Fence includes an internal list of always-protected targets (e.g. shell configs, git hooks) to reduce common persistence vectors.
|
- Fence includes an internal list of always-protected targets (e.g. shell configs, git hooks) to reduce common persistence vectors.
|
||||||
|
|
||||||
|
### Environment sanitization
|
||||||
|
|
||||||
|
Fence strips dangerous environment variables before passing them to sandboxed commands:
|
||||||
|
|
||||||
|
- `LD_*` (Linux): `LD_PRELOAD`, `LD_LIBRARY_PATH`, etc.
|
||||||
|
- `DYLD_*` (macOS): `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, etc.
|
||||||
|
|
||||||
|
This prevents a library injection attack where a sandboxed process writes a malicious `.so`/`.dylib` and then uses `LD_PRELOAD`/`DYLD_INSERT_LIBRARIES` in a subsequent command to load it.
|
||||||
|
|
||||||
## Visibility / auditing
|
## Visibility / auditing
|
||||||
|
|
||||||
- `-m/--monitor` helps you discover what a command *tries* to access (blocked only).
|
- `-m/--monitor` helps you discover what a command *tries* to access (blocked only).
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -18,9 +17,9 @@ type CommandBlockedError struct {
|
|||||||
|
|
||||||
func (e *CommandBlockedError) Error() string {
|
func (e *CommandBlockedError) Error() string {
|
||||||
if e.IsDefault {
|
if e.IsDefault {
|
||||||
return fmt.Sprintf("command blocked by default policy: %q matches %q", e.Command, e.BlockedPrefix)
|
return fmt.Sprintf("command blocked by default sandbox command policy: %q matches %q", e.Command, e.BlockedPrefix)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("command blocked by policy: %q matches %q", e.Command, e.BlockedPrefix)
|
return fmt.Sprintf("command blocked by sandbox command policy: %q matches %q", e.Command, e.BlockedPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckCommand checks if a command is allowed by the configuration.
|
// CheckCommand checks if a command is allowed by the configuration.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build !linux
|
//go:build !linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build !linux
|
//go:build !linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
// LinuxFeatures describes available Linux sandboxing features.
|
// LinuxFeatures describes available Linux sandboxing features.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build !linux
|
//go:build !linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import "github.com/Use-Tusk/fence/internal/config"
|
import "github.com/Use-Tusk/fence/internal/config"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//go:build !linux
|
//go:build !linux
|
||||||
|
|
||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
// SeccompFilter is a stub for non-Linux platforms.
|
// SeccompFilter is a stub for non-Linux platforms.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
114
internal/sandbox/sanitize.go
Normal file
114
internal/sandbox/sanitize.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DangerousEnvPrefixes lists environment variable prefixes that can be used
|
||||||
|
// to subvert library loading and should be stripped from sandboxed processes.
|
||||||
|
//
|
||||||
|
// - LD_* (Linux): LD_PRELOAD, LD_LIBRARY_PATH can inject malicious shared libraries
|
||||||
|
// - DYLD_* (macOS): DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH can inject dylibs
|
||||||
|
var DangerousEnvPrefixes = []string{
|
||||||
|
"LD_", // Linux dynamic linker
|
||||||
|
"DYLD_", // macOS dynamic linker
|
||||||
|
}
|
||||||
|
|
||||||
|
// DangerousEnvVars lists specific environment variables that should be stripped.
|
||||||
|
var DangerousEnvVars = []string{
|
||||||
|
"LD_PRELOAD",
|
||||||
|
"LD_LIBRARY_PATH",
|
||||||
|
"LD_AUDIT",
|
||||||
|
"LD_DEBUG",
|
||||||
|
"LD_DEBUG_OUTPUT",
|
||||||
|
"LD_DYNAMIC_WEAK",
|
||||||
|
"LD_ORIGIN_PATH",
|
||||||
|
"LD_PROFILE",
|
||||||
|
"LD_PROFILE_OUTPUT",
|
||||||
|
"LD_SHOW_AUXV",
|
||||||
|
"LD_TRACE_LOADED_OBJECTS",
|
||||||
|
"DYLD_INSERT_LIBRARIES",
|
||||||
|
"DYLD_LIBRARY_PATH",
|
||||||
|
"DYLD_FRAMEWORK_PATH",
|
||||||
|
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||||
|
"DYLD_FALLBACK_FRAMEWORK_PATH",
|
||||||
|
"DYLD_IMAGE_SUFFIX",
|
||||||
|
"DYLD_FORCE_FLAT_NAMESPACE",
|
||||||
|
"DYLD_PRINT_LIBRARIES",
|
||||||
|
"DYLD_PRINT_APIS",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHardenedEnv returns a copy of the current environment with dangerous
|
||||||
|
// variables removed. This prevents library injection attacks where a malicious
|
||||||
|
// agent writes a .so/.dylib and then uses LD_PRELOAD/DYLD_INSERT_LIBRARIES
|
||||||
|
// in a subsequent command.
|
||||||
|
func GetHardenedEnv() []string {
|
||||||
|
return FilterDangerousEnv(os.Environ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterDangerousEnv filters out dangerous environment variables from the given slice.
|
||||||
|
func FilterDangerousEnv(env []string) []string {
|
||||||
|
filtered := make([]string, 0, len(env))
|
||||||
|
for _, e := range env {
|
||||||
|
if !isDangerousEnvVar(e) {
|
||||||
|
filtered = append(filtered, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDangerousEnvVar checks if an environment variable entry (KEY=VALUE) is dangerous.
|
||||||
|
func isDangerousEnvVar(entry string) bool {
|
||||||
|
// Split on first '=' to get the key
|
||||||
|
key := entry
|
||||||
|
if idx := strings.Index(entry, "="); idx != -1 {
|
||||||
|
key = entry[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against known dangerous prefixes
|
||||||
|
for _, prefix := range DangerousEnvPrefixes {
|
||||||
|
if strings.HasPrefix(key, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against specific dangerous vars
|
||||||
|
for _, dangerous := range DangerousEnvVars {
|
||||||
|
if key == dangerous {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrippedEnvVars returns a list of environment variable names that were
|
||||||
|
// stripped from the given environment. Useful for debug logging.
|
||||||
|
func GetStrippedEnvVars(env []string) []string {
|
||||||
|
var stripped []string
|
||||||
|
for _, e := range env {
|
||||||
|
if isDangerousEnvVar(e) {
|
||||||
|
// Extract just the key
|
||||||
|
if idx := strings.Index(e, "="); idx != -1 {
|
||||||
|
stripped = append(stripped, e[:idx])
|
||||||
|
} else {
|
||||||
|
stripped = append(stripped, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
|
||||||
|
// HardeningFeatures returns a description of environment sanitization applied on this platform.
|
||||||
|
func HardeningFeatures() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
return "env-filter(LD_*)"
|
||||||
|
case "darwin":
|
||||||
|
return "env-filter(DYLD_*)"
|
||||||
|
default:
|
||||||
|
return "env-filter"
|
||||||
|
}
|
||||||
|
}
|
||||||
156
internal/sandbox/sanitize_test.go
Normal file
156
internal/sandbox/sanitize_test.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsDangerousEnvVar(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
entry string
|
||||||
|
dangerous bool
|
||||||
|
}{
|
||||||
|
// Linux LD_* variables
|
||||||
|
{"LD_PRELOAD=/tmp/evil.so", true},
|
||||||
|
{"LD_LIBRARY_PATH=/tmp", true},
|
||||||
|
{"LD_AUDIT=/tmp/audit.so", true},
|
||||||
|
{"LD_DEBUG=all", true},
|
||||||
|
|
||||||
|
// macOS DYLD_* variables
|
||||||
|
{"DYLD_INSERT_LIBRARIES=/tmp/evil.dylib", true},
|
||||||
|
{"DYLD_LIBRARY_PATH=/tmp", true},
|
||||||
|
{"DYLD_FRAMEWORK_PATH=/tmp", true},
|
||||||
|
{"DYLD_FORCE_FLAT_NAMESPACE=1", true},
|
||||||
|
|
||||||
|
// Safe variables
|
||||||
|
{"PATH=/usr/bin:/bin", false},
|
||||||
|
{"HOME=/home/user", false},
|
||||||
|
{"USER=user", false},
|
||||||
|
{"SHELL=/bin/bash", false},
|
||||||
|
{"HTTP_PROXY=http://localhost:8080", false},
|
||||||
|
{"HTTPS_PROXY=http://localhost:8080", false},
|
||||||
|
|
||||||
|
// Edge cases - variables that start with similar prefixes but aren't dangerous
|
||||||
|
{"LDFLAGS=-L/usr/lib", false}, // Not LD_ prefix
|
||||||
|
{"DISPLAY=:0", false},
|
||||||
|
|
||||||
|
// Empty and malformed
|
||||||
|
{"LD_PRELOAD", true}, // No value but still dangerous
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.entry, func(t *testing.T) {
|
||||||
|
got := isDangerousEnvVar(tt.entry)
|
||||||
|
if got != tt.dangerous {
|
||||||
|
t.Errorf("isDangerousEnvVar(%q) = %v, want %v", tt.entry, got, tt.dangerous)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterDangerousEnv(t *testing.T) {
|
||||||
|
env := []string{
|
||||||
|
"PATH=/usr/bin:/bin",
|
||||||
|
"LD_PRELOAD=/tmp/evil.so",
|
||||||
|
"HOME=/home/user",
|
||||||
|
"DYLD_INSERT_LIBRARIES=/tmp/evil.dylib",
|
||||||
|
"HTTP_PROXY=http://localhost:8080",
|
||||||
|
"LD_LIBRARY_PATH=/tmp",
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := FilterDangerousEnv(env)
|
||||||
|
|
||||||
|
// Should have 3 safe vars
|
||||||
|
if len(filtered) != 3 {
|
||||||
|
t.Errorf("expected 3 safe vars, got %d: %v", len(filtered), filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the safe vars are present
|
||||||
|
expected := map[string]bool{
|
||||||
|
"PATH=/usr/bin:/bin": true,
|
||||||
|
"HOME=/home/user": true,
|
||||||
|
"HTTP_PROXY=http://localhost:8080": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range filtered {
|
||||||
|
if !expected[e] {
|
||||||
|
t.Errorf("unexpected var in filtered env: %s", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dangerous vars are gone
|
||||||
|
for _, e := range filtered {
|
||||||
|
if isDangerousEnvVar(e) {
|
||||||
|
t.Errorf("dangerous var not filtered: %s", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStrippedEnvVars(t *testing.T) {
|
||||||
|
env := []string{
|
||||||
|
"PATH=/usr/bin",
|
||||||
|
"LD_PRELOAD=/tmp/evil.so",
|
||||||
|
"DYLD_INSERT_LIBRARIES=/tmp/evil.dylib",
|
||||||
|
"HOME=/home/user",
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped := GetStrippedEnvVars(env)
|
||||||
|
|
||||||
|
if len(stripped) != 2 {
|
||||||
|
t.Errorf("expected 2 stripped vars, got %d: %v", len(stripped), stripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain just the keys, not values
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for _, s := range stripped {
|
||||||
|
found[s] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found["LD_PRELOAD"] {
|
||||||
|
t.Error("expected LD_PRELOAD to be in stripped list")
|
||||||
|
}
|
||||||
|
if !found["DYLD_INSERT_LIBRARIES"] {
|
||||||
|
t.Error("expected DYLD_INSERT_LIBRARIES to be in stripped list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterDangerousEnv_EmptyInput(t *testing.T) {
|
||||||
|
filtered := FilterDangerousEnv(nil)
|
||||||
|
if filtered == nil {
|
||||||
|
t.Error("expected non-nil slice for nil input")
|
||||||
|
}
|
||||||
|
if len(filtered) != 0 {
|
||||||
|
t.Errorf("expected empty slice, got %v", filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = FilterDangerousEnv([]string{})
|
||||||
|
if len(filtered) != 0 {
|
||||||
|
t.Errorf("expected empty slice, got %v", filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterDangerousEnv_AllDangerous(t *testing.T) {
|
||||||
|
env := []string{
|
||||||
|
"LD_PRELOAD=/tmp/evil.so",
|
||||||
|
"LD_LIBRARY_PATH=/tmp",
|
||||||
|
"DYLD_INSERT_LIBRARIES=/tmp/evil.dylib",
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := FilterDangerousEnv(env)
|
||||||
|
if len(filtered) != 0 {
|
||||||
|
t.Errorf("expected all vars to be filtered, got %v", filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterDangerousEnv_AllSafe(t *testing.T) {
|
||||||
|
env := []string{
|
||||||
|
"PATH=/usr/bin",
|
||||||
|
"HOME=/home/user",
|
||||||
|
"USER=test",
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := FilterDangerousEnv(env)
|
||||||
|
if len(filtered) != 3 {
|
||||||
|
t.Errorf("expected all 3 vars to pass through, got %d", len(filtered))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
|
||||||
package sandbox
|
package sandbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user