Add environment sanitization
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
@@ -18,9 +17,9 @@ type CommandBlockedError struct {
|
||||
|
||||
func (e *CommandBlockedError) Error() string {
|
||||
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.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build !linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import "time"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build !linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
// LinuxFeatures describes available Linux sandboxing features.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build !linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import "github.com/Use-Tusk/fence/internal/config"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//go:build !linux
|
||||
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
// SeccompFilter is a stub for non-Linux platforms.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package sandbox provides sandboxing functionality for macOS and Linux.
|
||||
package sandbox
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user