Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion internal/executor/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"os/exec"
"sync"

"github.com/watany-dev/raptor/internal/security"
)

// HostExecutor executes commands on the host system using a shell.
Expand All @@ -21,9 +23,11 @@ func NewHostExecutor() *HostExecutor {

// getCachedSysEnv returns the cached system environment variables.
// The cache is populated on first call using sync.Once for thread safety.
// Sensitive environment variables (credentials, tokens, etc.) are filtered out
// to prevent credential leakage to workflow commands.
func (h *HostExecutor) getCachedSysEnv() []string {
h.once.Do(func() {
h.cachedSysEnv = os.Environ()
h.cachedSysEnv = security.FilterSensitiveEnvVars(os.Environ())
})
return h.cachedSysEnv
}
Expand Down
123 changes: 123 additions & 0 deletions internal/security/envvar.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,129 @@ func ValidateEnvVarName(name string) error {
return nil
}

// SensitiveEnvVarPatterns contains patterns for environment variables that may contain secrets.
// These are filtered from inherited environment to prevent credential leakage.
var SensitiveEnvVarPatterns = []string{
// AWS credentials
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_SECURITY_TOKEN",

// Google Cloud
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_CLOUD_PROJECT",
"CLOUDSDK_AUTH_ACCESS_TOKEN",

// Azure
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_TENANT_ID",
"AZURE_SUBSCRIPTION_ID",

// Git/GitHub/GitLab
"GITHUB_TOKEN",
"GH_TOKEN",
"GITLAB_TOKEN",
"BITBUCKET_TOKEN",
"GIT_ASKPASS",
"GIT_CREDENTIAL_",

// SSH/GPG agents
"SSH_AUTH_SOCK",
"SSH_AGENT_PID",
"GPG_AGENT_INFO",
"GPG_TTY",

// Database credentials
"DATABASE_URL",
"DB_PASSWORD",
"POSTGRES_PASSWORD",
"MYSQL_PASSWORD",
"REDIS_PASSWORD",
"MONGODB_URI",

// API keys and tokens (common patterns)
"API_KEY",
"API_SECRET",
"AUTH_TOKEN",
"ACCESS_TOKEN",
"REFRESH_TOKEN",
"BEARER_TOKEN",

// NPM/Package managers
"NPM_TOKEN",
"NPM_AUTH_TOKEN",
"YARN_AUTH_TOKEN",
"NUGET_API_KEY",
"PYPI_TOKEN",
"RUBYGEMS_API_KEY",

// Docker/Container registries
"DOCKER_PASSWORD",
"DOCKER_AUTH_CONFIG",
"REGISTRY_PASSWORD",

// CI/CD
"CI_JOB_TOKEN",
"CIRCLE_TOKEN",
"TRAVIS_TOKEN",

// Encryption keys
"ENCRYPTION_KEY",
"SIGNING_KEY",
"PRIVATE_KEY",
"SECRET_KEY",

// General patterns (checked as suffix/contains)
"_SECRET",
"_PASSWORD",
"_TOKEN",
"_CREDENTIALS",
"_API_KEY",
"_PRIVATE_KEY",
}

// FilterSensitiveEnvVars filters out sensitive environment variables from the given list.
// It returns a new slice with only non-sensitive variables.
func FilterSensitiveEnvVars(envVars []string) []string {
filtered := make([]string, 0, len(envVars))
for _, env := range envVars {
// Split into key=value
idx := strings.Index(env, "=")
if idx == -1 {
continue // Invalid format, skip
}
key := strings.ToUpper(env[:idx])

if !isSensitiveEnvVar(key) {
filtered = append(filtered, env)
}
}
return filtered
}

// isSensitiveEnvVar checks if an environment variable name matches sensitive patterns.
func isSensitiveEnvVar(name string) bool {
upperName := strings.ToUpper(name)

for _, pattern := range SensitiveEnvVarPatterns {
// Exact match
if upperName == pattern {
return true
}
// Suffix match (for patterns like _SECRET, _PASSWORD)
if strings.HasPrefix(pattern, "_") && strings.HasSuffix(upperName, pattern) {
return true
}
// Prefix match (for patterns like GIT_CREDENTIAL_)
if strings.HasSuffix(pattern, "_") && strings.HasPrefix(upperName, pattern) {
return true
}
}
return false
}

// ValidateEnvVarValue validates that an environment variable value is safe.
func ValidateEnvVarValue(name, value string) error {
// Value length limit (DoS prevention)
Expand Down
211 changes: 211 additions & 0 deletions internal/security/envvar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,214 @@ func TestBlockedEnvVars_AllHaveReasons(t *testing.T) {
}
}
}

func TestFilterSensitiveEnvVars(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "empty input",
input: []string{},
expected: []string{},
},
{
name: "no sensitive vars",
input: []string{
"PATH=/usr/bin",
"HOME=/home/user",
"SHELL=/bin/bash",
},
expected: []string{
"PATH=/usr/bin",
"HOME=/home/user",
"SHELL=/bin/bash",
},
},
{
name: "filter AWS credentials",
input: []string{
"PATH=/usr/bin",
"AWS_ACCESS_KEY_ID=AKIA...",
"AWS_SECRET_ACCESS_KEY=secret",
"HOME=/home/user",
},
expected: []string{
"PATH=/usr/bin",
"HOME=/home/user",
},
},
{
name: "filter GitHub tokens",
input: []string{
"PATH=/usr/bin",
"GITHUB_TOKEN=ghp_xxxx",
"GH_TOKEN=ghp_yyyy",
},
expected: []string{
"PATH=/usr/bin",
},
},
{
name: "filter SSH agent",
input: []string{
"SSH_AUTH_SOCK=/tmp/ssh-xxx/agent.123",
"SSH_AGENT_PID=12345",
"TERM=xterm",
},
expected: []string{
"TERM=xterm",
},
},
{
name: "filter suffix patterns",
input: []string{
"MY_SECRET=hidden",
"DB_PASSWORD=pass123",
"AUTH_TOKEN=abc",
"NORMAL_VAR=value",
},
expected: []string{
"NORMAL_VAR=value",
},
},
{
name: "filter prefix patterns",
input: []string{
"GIT_CREDENTIAL_HELPER=store",
"GIT_CREDENTIAL_CACHE=cache",
"GIT_AUTHOR_NAME=user",
},
expected: []string{
"GIT_AUTHOR_NAME=user",
},
},
{
name: "case insensitive filtering",
input: []string{
"aws_access_key_id=key",
"Aws_Secret_Access_Key=secret",
"github_token=token",
},
expected: []string{},
},
{
name: "skip invalid format",
input: []string{
"VALID=value",
"INVALID_NO_EQUALS",
"ANOTHER=ok",
},
expected: []string{
"VALID=value",
"ANOTHER=ok",
},
},
{
name: "filter database URLs",
input: []string{
"DATABASE_URL=postgres://user:pass@host/db",
"MONGODB_URI=mongodb://localhost",
"REDIS_URL=redis://localhost",
},
expected: []string{
"REDIS_URL=redis://localhost",
},
},
{
name: "filter API keys",
input: []string{
"API_KEY=xxx",
"API_SECRET=yyy",
"STRIPE_API_KEY=sk_test",
"SERVICE_NAME=myapp",
},
expected: []string{
"SERVICE_NAME=myapp",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FilterSensitiveEnvVars(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("FilterSensitiveEnvVars() got %d items, want %d", len(result), len(tt.expected))
t.Errorf("got: %v", result)
t.Errorf("want: %v", tt.expected)
return
}
for i, v := range result {
if v != tt.expected[i] {
t.Errorf("FilterSensitiveEnvVars()[%d] = %q, want %q", i, v, tt.expected[i])
}
}
})
}
}

func TestIsSensitiveEnvVar(t *testing.T) {
tests := []struct {
name string
varName string
expected bool
}{
// Exact matches
{"AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID", true},
{"AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY", true},
{"GITHUB_TOKEN", "GITHUB_TOKEN", true},
{"SSH_AUTH_SOCK", "SSH_AUTH_SOCK", true},
{"DATABASE_URL", "DATABASE_URL", true},

// Case insensitive
{"lowercase aws key", "aws_access_key_id", true},
{"mixed case github token", "GitHub_Token", true},

// Suffix patterns
{"custom secret", "MY_CUSTOM_SECRET", true},
{"custom password", "DB_PASSWORD", true},
{"custom token", "OAUTH_TOKEN", true},
{"custom credentials", "APP_CREDENTIALS", true},
{"custom api key", "STRIPE_API_KEY", true},

// Prefix patterns
{"git credential helper", "GIT_CREDENTIAL_HELPER", true},
{"git credential cache", "GIT_CREDENTIAL_STORE", true},

// Non-sensitive
{"PATH", "PATH", false},
{"HOME", "HOME", false},
{"USER", "USER", false},
{"SHELL", "SHELL", false},
{"TERM", "TERM", false},
{"LANG", "LANG", false},
{"EDITOR", "EDITOR", false},
{"PWD", "PWD", false},
{"GOPATH", "GOPATH", false},

// Edge cases - not sensitive despite similar names
{"TOKENIZER", "TOKENIZER", false},
{"PASSWORD_MIN_LENGTH", "PASSWORD_MIN_LENGTH", false},
{"SECRET_MODE", "SECRET_MODE", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isSensitiveEnvVar(tt.varName)
if result != tt.expected {
t.Errorf("isSensitiveEnvVar(%q) = %v, want %v", tt.varName, result, tt.expected)
}
})
}
}

func TestSensitiveEnvVarPatterns_NoDuplicates(t *testing.T) {
seen := make(map[string]bool)
for _, pattern := range SensitiveEnvVarPatterns {
if seen[pattern] {
t.Errorf("Duplicate pattern found: %q", pattern)
}
seen[pattern] = true
}
}