diff --git a/conf/config.yaml b/conf/config.yaml index c665fc95..e576ca1e 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -14,3 +14,5 @@ proxy: socks5: '' http: '' https: '' + no_proxy: '' +allowed_env_vars: [] # list of environment variable names to propagate from the container to the script execution environment diff --git a/internal/core/runner/nodejs/nodejs.go b/internal/core/runner/nodejs/nodejs.go index e6f667a4..2d3b649f 100644 --- a/internal/core/runner/nodejs/nodejs.go +++ b/internal/core/runner/nodejs/nodejs.go @@ -93,6 +93,28 @@ func (p *NodeJsRunner) Run( } cmd.ExtraFiles = []*os.File{codeReader} + if configuration.Proxy.Socks5 != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", configuration.Proxy.Socks5)) + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Socks5)) + } else if configuration.Proxy.Https != "" || configuration.Proxy.Http != "" { + if configuration.Proxy.Https != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", configuration.Proxy.Https)) + } + if configuration.Proxy.Http != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Http)) + } + } + + if configuration.Proxy.NoProxy != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("NO_PROXY=%s", configuration.Proxy.NoProxy)) + } + + for _, envVar := range configuration.AllowedEnvVars { + if val := os.Getenv(envVar); val != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envVar, val)) + } + } + if len(configuration.AllowedSyscalls) > 0 { cmd.Env = append( cmd.Env, diff --git a/internal/core/runner/python/python.go b/internal/core/runner/python/python.go index dea3ef40..16d77961 100644 --- a/internal/core/runner/python/python.go +++ b/internal/core/runner/python/python.go @@ -91,6 +91,16 @@ func (p *PythonRunner) Run( } } + if configuration.Proxy.NoProxy != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("NO_PROXY=%s", configuration.Proxy.NoProxy)) + } + + for _, envVar := range configuration.AllowedEnvVars { + if val := os.Getenv(envVar); val != "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envVar, val)) + } + } + if len(configuration.AllowedSyscalls) > 0 { cmd.Env = append(cmd.Env, fmt.Sprintf("ALLOWED_SYSCALLS=%s", diff --git a/internal/static/config.go b/internal/static/config.go index bf301511..0da85f73 100644 --- a/internal/static/config.go +++ b/internal/static/config.go @@ -178,7 +178,29 @@ func InitConfig(path string) error { if difySandboxGlobalConfigurations.Proxy.Http != "" { slog.Info("using http proxy", "proxy", difySandboxGlobalConfigurations.Proxy.Http) } + + no_proxy := os.Getenv("NO_PROXY") + if no_proxy != "" { + difySandboxGlobalConfigurations.Proxy.NoProxy = no_proxy + } + + if difySandboxGlobalConfigurations.Proxy.NoProxy != "" { + slog.Info("using no proxy", "no_proxy", difySandboxGlobalConfigurations.Proxy.NoProxy) + } + } + + allowed_env_vars := os.Getenv("ALLOWED_ENV_VARS") + if allowed_env_vars != "" { + parts := strings.Split(allowed_env_vars, ",") + difySandboxGlobalConfigurations.AllowedEnvVars = make([]string, 0, len(parts)) + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + difySandboxGlobalConfigurations.AllowedEnvVars = append(difySandboxGlobalConfigurations.AllowedEnvVars, trimmed) + } + } } + return nil } diff --git a/internal/static/config_test.go b/internal/static/config_test.go index 53e7b2ef..b7d452d8 100644 --- a/internal/static/config_test.go +++ b/internal/static/config_test.go @@ -312,3 +312,146 @@ func writeTempConfig(t *testing.T, content string) string { return configPath } + +func TestInitConfigNoProxyFromEnv(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nenable_network: true\n") + + t.Setenv("SOCKS5_PROXY", "") + t.Setenv("HTTPS_PROXY", "") + t.Setenv("HTTP_PROXY", "") + t.Setenv("NO_PROXY", "localhost,127.0.0.1") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if config.Proxy.NoProxy != "localhost,127.0.0.1" { + t.Fatalf("expected NO_PROXY to be propagated, got %q", config.Proxy.NoProxy) + } +} + +func TestInitConfigNoProxyFromYAML(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nenable_network: true\nproxy:\n no_proxy: 'internal.example.com'\n") + + t.Setenv("NO_PROXY", "") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if config.Proxy.NoProxy != "internal.example.com" { + t.Fatalf("expected no_proxy from YAML, got %q", config.Proxy.NoProxy) + } +} + +func TestInitConfigNoProxyEnvOverridesYAML(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nenable_network: true\nproxy:\n no_proxy: 'from-yaml'\n") + + t.Setenv("NO_PROXY", "from-env") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if config.Proxy.NoProxy != "from-env" { + t.Fatalf("expected NO_PROXY env to override YAML, got %q", config.Proxy.NoProxy) + } +} + +func TestInitConfigNoProxyIgnoredWhenNetworkDisabled(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nenable_network: false\n") + + t.Setenv("NO_PROXY", "should-be-ignored") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if config.Proxy.NoProxy != "" { + t.Fatalf("expected NO_PROXY to be empty when network disabled, got %q", config.Proxy.NoProxy) + } +} + +func TestInitConfigAllowedEnvVarsFromEnv(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\n") + + t.Setenv("ALLOWED_ENV_VARS", "MY_VAR, ANOTHER_VAR , THIRD_VAR") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + want := []string{"MY_VAR", "ANOTHER_VAR", "THIRD_VAR"} + if len(config.AllowedEnvVars) != len(want) { + t.Fatalf("expected AllowedEnvVars %v, got %v", want, config.AllowedEnvVars) + } + for i, v := range want { + if config.AllowedEnvVars[i] != v { + t.Fatalf("AllowedEnvVars[%d]: expected %q, got %q", i, v, config.AllowedEnvVars[i]) + } + } +} + +func TestInitConfigAllowedEnvVarsFromYAML(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nallowed_env_vars:\n - FOO\n - BAR\n") + + t.Setenv("ALLOWED_ENV_VARS", "") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + want := []string{"FOO", "BAR"} + if len(config.AllowedEnvVars) != len(want) { + t.Fatalf("expected AllowedEnvVars %v, got %v", want, config.AllowedEnvVars) + } + for i, v := range want { + if config.AllowedEnvVars[i] != v { + t.Fatalf("AllowedEnvVars[%d]: expected %q, got %q", i, v, config.AllowedEnvVars[i]) + } + } +} + +func TestInitConfigAllowedEnvVarsEnvOverridesYAML(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\nallowed_env_vars:\n - FROM_YAML\n") + + t.Setenv("ALLOWED_ENV_VARS", "FROM_ENV") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if len(config.AllowedEnvVars) != 1 || config.AllowedEnvVars[0] != "FROM_ENV" { + t.Fatalf("expected ALLOWED_ENV_VARS env to override YAML, got %v", config.AllowedEnvVars) + } +} + +func TestInitConfigAllowedEnvVarsEmptyByDefault(t *testing.T) { + pythonPath := mustFindTestPython(t) + configPath := writeTempConfig(t, "app:\n port: 8194\npython_path: "+pythonPath+"\n") + + t.Setenv("ALLOWED_ENV_VARS", "") + + if err := InitConfig(configPath); err != nil { + t.Fatalf("InitConfig returned error: %v", err) + } + + config := GetDifySandboxGlobalConfigurations() + if len(config.AllowedEnvVars) != 0 { + t.Fatalf("expected AllowedEnvVars to be empty by default, got %v", config.AllowedEnvVars) + } +} diff --git a/internal/types/config.go b/internal/types/config.go index 23739cfb..dc0290b6 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -22,8 +22,10 @@ type DifySandboxGlobalConfigurations struct { AllowedSyscalls []int `yaml:"allowed_syscalls"` LogPath string `yaml:"log_path"` Proxy struct { - Socks5 string `yaml:"socks5"` - Https string `yaml:"https"` - Http string `yaml:"http"` + Socks5 string `yaml:"socks5"` + Https string `yaml:"https"` + Http string `yaml:"http"` + NoProxy string `yaml:"no_proxy"` } `yaml:"proxy"` + AllowedEnvVars []string `yaml:"allowed_env_vars"` } diff --git a/tests/integration_tests/conf/config.yaml b/tests/integration_tests/conf/config.yaml index 4e324ef1..f9a1ab81 100644 --- a/tests/integration_tests/conf/config.yaml +++ b/tests/integration_tests/conf/config.yaml @@ -8,7 +8,10 @@ worker_timeout: 30 python_path: /opt/python/bin/python3 enable_network: True # please make sure there is no network risk in your environment allowed_syscalls: # please leave it empty if you have no idea how seccomp works +allowed_env_vars: + - TEST_SANDBOX_ENV_VAR proxy: socks5: '' http: '' https: '' + no_proxy: 'test.no-proxy.internal' diff --git a/tests/integration_tests/nodejs_feature_test.go b/tests/integration_tests/nodejs_feature_test.go index 9ea962f2..52ef871d 100644 --- a/tests/integration_tests/nodejs_feature_test.go +++ b/tests/integration_tests/nodejs_feature_test.go @@ -159,3 +159,63 @@ throw new Error("bad input"); t.Fatalf("expected non-zero exit code, got: %d\n", data.ExitCode) } } +func TestNodejsNoProxyEnvPropagation(t *testing.T) { + resp := service.RunNodeJsCode(context.TODO(), ` +console.log(process.env.NO_PROXY || ''); + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if !strings.Contains(data.Stdout, "test.no-proxy.internal") { + t.Fatalf("expected NO_PROXY to be propagated to subprocess, got: %q\n", data.Stdout) + } +} + +func TestNodejsAllowedEnvVarsPropagation(t *testing.T) { + t.Setenv("TEST_SANDBOX_ENV_VAR", "hello_from_allowed_env") + + resp := service.RunNodeJsCode(context.TODO(), ` +console.log(process.env.TEST_SANDBOX_ENV_VAR || ''); + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if !strings.Contains(data.Stdout, "hello_from_allowed_env") { + t.Fatalf("expected TEST_SANDBOX_ENV_VAR to be propagated to subprocess, got: %q\n", data.Stdout) + } +} + +func TestNodejsUnlistedEnvVarNotPropagated(t *testing.T) { + t.Setenv("UNLISTED_ENV_VAR", "should_not_appear") + + resp := service.RunNodeJsCode(context.TODO(), ` +console.log(process.env.UNLISTED_ENV_VAR || 'not_found'); + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if strings.Contains(data.Stdout, "should_not_appear") { + t.Fatalf("expected UNLISTED_ENV_VAR NOT to be propagated, but it was: %q\n", data.Stdout) + } +} diff --git a/tests/integration_tests/python_feature_test.go b/tests/integration_tests/python_feature_test.go index 1b26c25f..e2761a68 100644 --- a/tests/integration_tests/python_feature_test.go +++ b/tests/integration_tests/python_feature_test.go @@ -213,3 +213,66 @@ raise ValueError("bad input") t.Fatalf("expected non-zero exit code, got: %d\n", data.ExitCode) } } +func TestPythonNoProxyEnvPropagation(t *testing.T) { + resp := service.RunPython3Code(context.TODO(), ` +import os +print(os.environ.get('NO_PROXY', '')) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if !strings.Contains(data.Stdout, "test.no-proxy.internal") { + t.Fatalf("expected NO_PROXY to be propagated to subprocess, got: %q\n", data.Stdout) + } +} + +func TestPythonAllowedEnvVarsPropagation(t *testing.T) { + t.Setenv("TEST_SANDBOX_ENV_VAR", "hello_from_allowed_env") + + resp := service.RunPython3Code(context.TODO(), ` +import os +print(os.environ.get('TEST_SANDBOX_ENV_VAR', '')) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if !strings.Contains(data.Stdout, "hello_from_allowed_env") { + t.Fatalf("expected TEST_SANDBOX_ENV_VAR to be propagated to subprocess, got: %q\n", data.Stdout) + } +} + +func TestPythonUnlistedEnvVarNotPropagated(t *testing.T) { + t.Setenv("UNLISTED_ENV_VAR", "should_not_appear") + + resp := service.RunPython3Code(context.TODO(), ` +import os +print(os.environ.get('UNLISTED_ENV_VAR', 'not_found')) + `, "", &types.RunnerOptions{ + EnableNetwork: true, + }) + if resp.Code != 0 { + t.Fatal(resp) + } + + data := resp.Data.(*service.RunCodeResponse) + if data.Stderr != "" { + t.Fatalf("unexpected stderr: %s\n", data.Stderr) + } + if strings.Contains(data.Stdout, "should_not_appear") { + t.Fatalf("expected UNLISTED_ENV_VAR NOT to be propagated, but it was: %q\n", data.Stdout) + } +}