From f6d2435a323d128b7efde195d1c5da4270b3ec9e Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sat, 8 Aug 2026 05:00:22 +0700 Subject: [PATCH] fix(local-runtime): stop inheriting daemon environment in plugin subprocesses getInstanceCmd built plugin processes with cmd.Environ(), copying the daemon's full environment (DB_PASSWORD, SERVER_KEY, DIFY_INNER_API_KEY, Redis and cloud storage credentials) into every plugin subprocess, where any installed plugin could read and exfiltrate it over the network. Replace inheritance with an explicit allowlist builder, BuildPluginCommandEnv, mirroring the existing buildUVCommandEnv pattern used for the uv installer child process. The allowlist passes through what plugins legitimately need (PATH, HOME, locale variables, temp directories, TZ, CA bundle and proxy variables), daemon config proxy settings take precedence over inherited ones, and INSTALL_METHOD=local is set as before. The slim CLI local mode used the same os.Environ() pattern for marketplace-downloaded plugins and now shares the builder. --- internal/core/local_runtime/subprocess.go | 68 +++++++++++-- .../core/local_runtime/subprocess_test.go | 98 +++++++++++++++++++ pkg/slim/local.go | 2 +- 3 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 internal/core/local_runtime/subprocess_test.go 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 {