diff --git a/internal/core/local_runtime/subprocess.go b/internal/core/local_runtime/subprocess.go index 411c7534b..09586df1b 100644 --- a/internal/core/local_runtime/subprocess.go +++ b/internal/core/local_runtime/subprocess.go @@ -9,6 +9,7 @@ import ( "slices" "time" + "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/pkg/entities/constants" "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" routinepkg "github.com/langgenius/dify-plugin-daemon/pkg/routine" @@ -32,19 +33,66 @@ func (r *LocalPluginRuntime) getInstanceCmd() (*exec.Cmd, error) { return nil, fmt.Errorf("unsupported language: %s", r.Config.Meta.Runner.Language) } - cmd.Env = cmd.Environ() - if r.appConfig.HttpsProxy != "" { - cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", r.appConfig.HttpsProxy)) + cmd.Env = BuildPluginCommandEnv(r.appConfig) + cmd.Dir = r.State.WorkingPath + return cmd, nil +} + +// pluginCommandEnvAllowlist lists the only process environment variables a +// plugin subprocess may inherit. Daemon credentials such as DB_PASSWORD, +// SERVER_KEY or DIFY_INNER_API_KEY must never reach plugin code. +var pluginCommandEnvAllowlist = []string{ + "PATH", + "HOME", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TMPDIR", + "TEMP", + "TMP", + "TZ", + "SSL_CERT_FILE", + "REQUESTS_CA_BUNDLE", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", +} + +// BuildPluginCommandEnv builds the environment of a plugin subprocess from an +// explicit allowlist instead of inheriting the daemon process environment, +// mirroring buildUVCommandEnv for the dependency installer. Proxy settings +// from the daemon config take precedence over inherited proxy variables. +func BuildPluginCommandEnv(appConfig *app.Config) []string { + envByKey := make(map[string]string, len(pluginCommandEnvAllowlist)+4) + for _, key := range pluginCommandEnvAllowlist { + if value, ok := os.LookupEnv(key); ok { + envByKey[key] = value + } } - if r.appConfig.HttpProxy != "" { - cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", r.appConfig.HttpProxy)) + + if appConfig != nil { + if appConfig.HttpProxy != "" { + envByKey["HTTP_PROXY"] = appConfig.HttpProxy + } + if appConfig.HttpsProxy != "" { + envByKey["HTTPS_PROXY"] = appConfig.HttpsProxy + } + if appConfig.NoProxy != "" { + envByKey["NO_PROXY"] = appConfig.NoProxy + } } - if r.appConfig.NoProxy != "" { - cmd.Env = append(cmd.Env, fmt.Sprintf("NO_PROXY=%s", r.appConfig.NoProxy)) + + envByKey["INSTALL_METHOD"] = "local" + + env := make([]string, 0, len(envByKey)) + for key, value := range envByKey { + env = append(env, key+"="+value) } - cmd.Env = append(cmd.Env, "INSTALL_METHOD=local", "PATH="+os.Getenv("PATH")) - cmd.Dir = r.State.WorkingPath - return cmd, nil + slices.Sort(env) + return env } // getInstanceStdio gets the stdin, stdout, and stderr pipes for the plugin instance diff --git a/internal/core/local_runtime/subprocess_test.go b/internal/core/local_runtime/subprocess_test.go new file mode 100644 index 000000000..498beb353 --- /dev/null +++ b/internal/core/local_runtime/subprocess_test.go @@ -0,0 +1,98 @@ +package local_runtime + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/langgenius/dify-plugin-daemon/internal/types/app" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/constants" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/stretchr/testify/require" +) + +func pluginEnvSliceToMap(t *testing.T, env []string) map[string]string { + t.Helper() + envByKey := make(map[string]string, len(env)) + for _, item := range env { + key, value, found := strings.Cut(item, "=") + require.True(t, found) + envByKey[key] = value + } + return envByKey +} + +func TestBuildPluginCommandEnv(t *testing.T) { + t.Setenv("PATH", "/test/bin") + t.Setenv("HOME", "/test/home") + t.Setenv("LANG", "en_US.UTF-8") + t.Setenv("HTTP_PROXY", "http://env-proxy:8080") + t.Setenv("DB_PASSWORD", "must-not-be-inherited") + t.Setenv("SERVER_KEY", "must-not-be-inherited") + t.Setenv("DIFY_INNER_API_KEY", "must-not-be-inherited") + t.Setenv("AWS_ACCESS_KEY_ID", "must-not-be-inherited") + t.Setenv("AWS_SECRET_ACCESS_KEY", "must-not-be-inherited") + t.Setenv("REDIS_PASSWORD", "must-not-be-inherited") + t.Setenv("ADMIN_API_KEY", "must-not-be-inherited") + t.Setenv("UNRELATED_SECRET", "must-not-be-inherited") + + env := BuildPluginCommandEnv(&app.Config{ + HttpsProxy: "https://config-proxy:8443", + NoProxy: "localhost,127.0.0.1", + }) + envByKey := pluginEnvSliceToMap(t, env) + + // allowlisted variables are passed through + require.Equal(t, "/test/bin", envByKey["PATH"]) + require.Equal(t, "/test/home", envByKey["HOME"]) + require.Equal(t, "en_US.UTF-8", envByKey["LANG"]) + require.Equal(t, "http://env-proxy:8080", envByKey["HTTP_PROXY"]) + require.Equal(t, "local", envByKey["INSTALL_METHOD"]) + + // proxy settings from the daemon config take precedence + require.Equal(t, "https://config-proxy:8443", envByKey["HTTPS_PROXY"]) + require.Equal(t, "localhost,127.0.0.1", envByKey["NO_PROXY"]) + + // daemon secrets never reach the plugin process + for _, key := range []string{ + "DB_PASSWORD", + "SERVER_KEY", + "DIFY_INNER_API_KEY", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "REDIS_PASSWORD", + "ADMIN_API_KEY", + "UNRELATED_SECRET", + } { + require.NotContains(t, envByKey, key) + } +} + +func TestGetInstanceCmdDoesNotInheritDaemonEnv(t *testing.T) { + t.Setenv("PATH", "/test/bin") + t.Setenv("DB_PASSWORD", "must-not-be-inherited") + t.Setenv("SERVER_KEY", "must-not-be-inherited") + t.Setenv("DIFY_INNER_API_KEY", "must-not-be-inherited") + + // minimal fake venv so getVirtualEnvironmentPythonPath succeeds + workDir := t.TempDir() + venvBin := filepath.Join(workDir, ".venv", "bin") + require.NoError(t, os.MkdirAll(venvBin, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte("#!/bin/sh\n"), 0o755)) + + r := &LocalPluginRuntime{appConfig: &app.Config{}} + r.State = plugin_entities.PluginRuntimeState{WorkingPath: workDir} + r.Config.Meta.Runner.Language = constants.Python + r.Config.Meta.Runner.Entrypoint = "main" + + cmd, err := r.getInstanceCmd() + require.NoError(t, err) + + envByKey := pluginEnvSliceToMap(t, cmd.Env) + require.Equal(t, "/test/bin", envByKey["PATH"]) + require.Equal(t, "local", envByKey["INSTALL_METHOD"]) + require.NotContains(t, envByKey, "DB_PASSWORD") + require.NotContains(t, envByKey, "SERVER_KEY") + require.NotContains(t, envByKey, "DIFY_INNER_API_KEY") +} diff --git a/pkg/slim/local.go b/pkg/slim/local.go index c1ca5a7e8..313ee670d 100644 --- a/pkg/slim/local.go +++ b/pkg/slim/local.go @@ -169,7 +169,7 @@ func execPlugin( cmd := exec.Command(pythonPath, "-m", rt.Config.Meta.Runner.Entrypoint) cmd.Dir = rt.State.WorkingPath - cmd.Env = append(os.Environ(), "INSTALL_METHOD=local", "PATH="+os.Getenv("PATH")) + cmd.Env = local_runtime.BuildPluginCommandEnv(appConfig) stdin, err := cmd.StdinPipe() if err != nil {