Skip to content

Commit 12a7673

Browse files
authored
feature/env vars filter (#11)
* Implement secure environment variable management - Added `secureenv` package for filtering environment variables based on configuration. - Introduced `EnvConfig` struct to manage allowed system variables and custom variables. - Updated `Config` struct to include environment configuration. - Enhanced `DefaultConfig` to initialize secure environment settings. - Integrated secure environment manager into upstream and client components. - Added tests for secure environment functionality and integration scenarios. * Enhance timeout management and logging in client and server components - Updated timeout duration for tool listing operations to 30 seconds for better handling of Docker container startup and API calls. - Improved logging for various operations, including connection errors and tool calls, to provide more detailed insights during debugging. - Added conditional logging for JSON request/response data based on debug level, enhancing traceability without cluttering logs. * Refactor code for clarity and consistency - Removed unnecessary blank lines in config validation and test functions. - Simplified environment variable splitting logic in tests. - Introduced constants for OS checks to improve readability and maintainability. - Updated autostart functionality checks to use constants for OS types. - Enhanced error handling in file operations for better robustness.
1 parent 56f6863 commit 12a7673

13 files changed

Lines changed: 1236 additions & 89 deletions

File tree

internal/config/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"encoding/json"
5+
"mcpproxy-go/internal/secureenv"
56
"time"
67
)
78

@@ -20,6 +21,9 @@ type Config struct {
2021
ToolsLimit int `json:"tools_limit" mapstructure:"tools-limit"`
2122
ToolResponseLimit int `json:"tool_response_limit" mapstructure:"tool-response-limit"`
2223

24+
// Environment configuration for secure variable filtering
25+
Environment *secureenv.EnvConfig `json:"environment,omitempty" mapstructure:"environment"`
26+
2327
// Logging configuration
2428
Logging *LogConfig `json:"logging,omitempty" mapstructure:"logging"`
2529

@@ -154,6 +158,9 @@ func DefaultConfig() *Config {
154158
ToolsLimit: 15,
155159
ToolResponseLimit: 20000, // Default 20000 characters
156160

161+
// Default secure environment configuration
162+
Environment: secureenv.DefaultEnvConfig(),
163+
157164
// Default logging configuration
158165
Logging: &LogConfig{
159166
Level: "info",
@@ -192,6 +199,12 @@ func (c *Config) Validate() error {
192199
if c.ToolResponseLimit < 0 {
193200
c.ToolResponseLimit = 0 // 0 means disabled
194201
}
202+
203+
// Ensure Environment config is not nil
204+
if c.Environment == nil {
205+
c.Environment = secureenv.DefaultEnvConfig()
206+
}
207+
195208
return nil
196209
}
197210

internal/config/loader.go

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -297,26 +297,21 @@ func LoadOrCreateConfig(dataDir string) (*Config, error) {
297297

298298
// CreateSampleConfig creates a sample configuration file
299299
func CreateSampleConfig(path string) error {
300-
cfg := &Config{
301-
Listen: ":8080",
302-
EnableTray: true,
303-
TopK: 5,
304-
ToolsLimit: 15,
305-
Servers: []*ServerConfig{
306-
{
307-
Name: "example",
308-
URL: "http://localhost:8000/mcp/",
309-
Enabled: true,
310-
Created: now(),
311-
},
312-
{
313-
Name: "local-command",
314-
Command: "mcp-server-example",
315-
Args: []string{"--config", "example.json"},
316-
Env: map[string]string{"DEBUG": "true"},
317-
Enabled: true,
318-
Created: now(),
319-
},
300+
cfg := DefaultConfig()
301+
cfg.Servers = []*ServerConfig{
302+
{
303+
Name: "example",
304+
URL: "http://localhost:8000/mcp/",
305+
Enabled: true,
306+
Created: now(),
307+
},
308+
{
309+
Name: "local-command",
310+
Command: "mcp-server-example",
311+
Args: []string{"--config", "example.json"},
312+
Env: map[string]string{"DEBUG": "true"},
313+
Enabled: true,
314+
Created: now(),
320315
},
321316
}
322317

internal/secureenv/manager.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package secureenv
2+
3+
import (
4+
"os"
5+
"runtime"
6+
"strings"
7+
)
8+
9+
const (
10+
osWindows = "windows"
11+
)
12+
13+
// EnvConfig represents environment configuration for secure filtering
14+
type EnvConfig struct {
15+
InheritSystemSafe bool `json:"inherit_system_safe"`
16+
AllowedSystemVars []string `json:"allowed_system_vars"`
17+
CustomVars map[string]string `json:"custom_vars"`
18+
}
19+
20+
// DefaultEnvConfig returns default environment configuration with safe system variables
21+
func DefaultEnvConfig() *EnvConfig {
22+
allowedVars := []string{
23+
"PATH", // Essential for finding executables
24+
"HOME", // User directory path (Unix)
25+
"TMPDIR", // Temporary directory (Unix)
26+
"TEMP", // Temporary directory (Windows)
27+
"TMP", // Temporary directory (Windows)
28+
"SHELL", // Default shell
29+
"TERM", // Terminal type
30+
"LANG", // Language settings
31+
"USER", // Current user (Unix)
32+
"USERNAME", // Current user (Windows)
33+
}
34+
35+
// Add Windows-specific variables
36+
if runtime.GOOS == osWindows {
37+
allowedVars = append(allowedVars,
38+
"USERPROFILE", // User profile directory
39+
"APPDATA", // Application data directory
40+
"LOCALAPPDATA", // Local application data directory
41+
"PROGRAMFILES", // Program files directory
42+
"SYSTEMROOT", // System root directory
43+
"COMSPEC", // Command interpreter
44+
)
45+
}
46+
47+
// Add Unix-specific variables
48+
if runtime.GOOS != osWindows {
49+
allowedVars = append(allowedVars,
50+
"XDG_CONFIG_HOME", // XDG config directory
51+
"XDG_DATA_HOME", // XDG data directory
52+
"XDG_CACHE_HOME", // XDG cache directory
53+
"XDG_RUNTIME_DIR", // XDG runtime directory
54+
)
55+
}
56+
57+
// Add locale-related variables
58+
localeVars := []string{
59+
"LC_ALL", "LC_CTYPE", "LC_NUMERIC", "LC_TIME", "LC_COLLATE",
60+
"LC_MONETARY", "LC_MESSAGES", "LC_PAPER", "LC_NAME", "LC_ADDRESS",
61+
"LC_TELEPHONE", "LC_MEASUREMENT", "LC_IDENTIFICATION",
62+
}
63+
allowedVars = append(allowedVars, localeVars...)
64+
65+
return &EnvConfig{
66+
InheritSystemSafe: true,
67+
AllowedSystemVars: allowedVars,
68+
CustomVars: make(map[string]string),
69+
}
70+
}
71+
72+
// Manager handles secure environment variable filtering
73+
type Manager struct {
74+
config *EnvConfig
75+
}
76+
77+
// NewManager creates a new secure environment manager
78+
func NewManager(config *EnvConfig) *Manager {
79+
if config == nil {
80+
config = DefaultEnvConfig()
81+
}
82+
return &Manager{config: config}
83+
}
84+
85+
// BuildSecureEnvironment builds a secure environment variable list
86+
func (m *Manager) BuildSecureEnvironment() []string {
87+
var envVars []string
88+
89+
// Add safe system environment variables if enabled
90+
if m.config.InheritSystemSafe {
91+
envVars = append(envVars, m.getFilteredSystemEnv()...)
92+
}
93+
94+
// Add custom environment variables from config
95+
for k, v := range m.config.CustomVars {
96+
envVars = append(envVars, k+"="+v)
97+
}
98+
99+
return envVars
100+
}
101+
102+
// getFilteredSystemEnv returns filtered system environment variables
103+
func (m *Manager) getFilteredSystemEnv() []string {
104+
systemEnv := os.Environ()
105+
var filtered []string
106+
107+
for _, envVar := range systemEnv {
108+
if m.isEnvVarAllowed(envVar) {
109+
filtered = append(filtered, envVar)
110+
}
111+
}
112+
113+
return filtered
114+
}
115+
116+
// isEnvVarAllowed checks if an environment variable is allowed based on the allow-list
117+
func (m *Manager) isEnvVarAllowed(envVar string) bool {
118+
parts := strings.SplitN(envVar, "=", 2)
119+
if len(parts) != 2 {
120+
return false
121+
}
122+
123+
key := parts[0]
124+
125+
// Check against allow-list
126+
for _, allowedVar := range m.config.AllowedSystemVars {
127+
if key == allowedVar {
128+
return true
129+
}
130+
131+
// Support wildcard matching for locale variables (LC_*)
132+
if strings.HasSuffix(allowedVar, "*") {
133+
prefix := strings.TrimSuffix(allowedVar, "*")
134+
if strings.HasPrefix(key, prefix) {
135+
return true
136+
}
137+
}
138+
}
139+
140+
return false
141+
}
142+
143+
// GetSystemEnvVar gets a specific system environment variable if allowed
144+
func (m *Manager) GetSystemEnvVar(key string) (string, bool) {
145+
if !m.isKeyAllowed(key) {
146+
return "", false
147+
}
148+
149+
value := os.Getenv(key)
150+
return value, value != ""
151+
}
152+
153+
// isKeyAllowed checks if a key is in the allow-list
154+
func (m *Manager) isKeyAllowed(key string) bool {
155+
for _, allowedVar := range m.config.AllowedSystemVars {
156+
if key == allowedVar {
157+
return true
158+
}
159+
160+
// Support wildcard matching
161+
if strings.HasSuffix(allowedVar, "*") {
162+
prefix := strings.TrimSuffix(allowedVar, "*")
163+
if strings.HasPrefix(key, prefix) {
164+
return true
165+
}
166+
}
167+
}
168+
169+
return false
170+
}
171+
172+
// ValidateConfig validates the environment configuration
173+
func (m *Manager) ValidateConfig() error {
174+
// Environment configuration is always valid with our allow-list approach
175+
// The worst case is an empty environment, which is still secure
176+
return nil
177+
}
178+
179+
// GetFilteredEnvCount returns the number of filtered environment variables
180+
func (m *Manager) GetFilteredEnvCount() (filteredCount, totalCount int) {
181+
systemEnv := os.Environ()
182+
filteredEnv := m.getFilteredSystemEnv()
183+
return len(filteredEnv), len(systemEnv)
184+
}

0 commit comments

Comments
 (0)