Skip to content
Merged
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
41 changes: 36 additions & 5 deletions core/plugins/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strconv"
"sync"
"time"
Expand Down Expand Up @@ -108,6 +109,40 @@ func (m *Manager) port(id string) int {
return 0
}

// envAllowList is what a plugin gets by default. LookupEnv simply misses
// whichever names don't apply to the current OS, so no runtime.GOOS branch
// is needed: PATH lets the plugin's own entry command find its runtime/
// tools; the rest are what node/python/go and Windows' network stack expect
// to find a home/cache/temp dir.
var envAllowList = []string{"PATH", "HOME", "USERPROFILE", "TMPDIR", "TEMP", "TMP", "SystemRoot"}

func minimalEnv() []string {
env := make([]string, 0, len(envAllowList))
for _, name := range envAllowList {
if v, ok := os.LookupEnv(name); ok {
env = append(env, name+"="+v)
}
}
return env
}

// buildEnv assembles a plugin's child-process environment. A plugin that
// declares "env:full" in Manifest.Permissions inherits the full parent
// environment (today's behavior); everything else gets envAllowList only,
// so it can't read ambient secrets out of enowX's own environment unless it
// explicitly opts in.
func buildEnv(man *Manifest, id string, port, enowxPort int) []string {
env := minimalEnv()
if slices.Contains(man.Permissions, "env:full") {
env = os.Environ()
}
return append(env,
"PORT="+strconv.Itoa(port),
"ENOWX_PLUGIN_ID="+id,
"ENOWX_API=http://127.0.0.1:"+strconv.Itoa(enowxPort),
)
}

// Start spawns a plugin's sidecar (no-op for static plugins).
func (m *Manager) Start(id string) error {
if err := m.authorized(); err != nil {
Expand Down Expand Up @@ -153,11 +188,7 @@ func (m *Manager) Start(id string) error {
}
cmd := exec.Command(bin, args...)
cmd.Dir = filepath.Join(m.dir, id)
cmd.Env = append(os.Environ(),
"PORT="+strconv.Itoa(port),
"ENOWX_PLUGIN_ID="+id,
"ENOWX_API=http://127.0.0.1:"+strconv.Itoa(m.enowxPort),
)
cmd.Env = buildEnv(man, id, port, m.enowxPort)
hideWindow(cmd)

pr := &proc{cmd: cmd, port: port, running: true}
Expand Down
57 changes: 57 additions & 0 deletions core/plugins/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package plugins

import (
"slices"
"strings"
"testing"
)

func hasEnvKey(env []string, key string) bool {
prefix := key + "="
return slices.ContainsFunc(env, func(kv string) bool { return strings.HasPrefix(kv, prefix) })
}

func TestMinimalEnv(t *testing.T) {
t.Setenv("PATH", "/usr/bin")
t.Setenv("ENOWX_TEST_SECRET", "top-secret")

env := minimalEnv()

if !hasEnvKey(env, "PATH") {
t.Errorf("minimalEnv() = %v, want PATH present", env)
}
if hasEnvKey(env, "ENOWX_TEST_SECRET") {
t.Errorf("minimalEnv() = %v, leaked a non-allow-listed var", env)
}
}

func TestBuildEnv(t *testing.T) {
t.Setenv("ENOWX_TEST_SECRET", "top-secret")

t.Run("default scopes down to the allow-list", func(t *testing.T) {
man := &Manifest{ID: "demo"}
env := buildEnv(man, "demo", 5555, 1430)

if hasEnvKey(env, "ENOWX_TEST_SECRET") {
t.Errorf("buildEnv() leaked ENOWX_TEST_SECRET into a plugin without env:full")
}
if !hasEnvKey(env, "PORT") {
t.Errorf("buildEnv() = %v, missing PORT", env)
}
if !hasEnvKey(env, "ENOWX_PLUGIN_ID") {
t.Errorf("buildEnv() = %v, missing ENOWX_PLUGIN_ID", env)
}
if !hasEnvKey(env, "ENOWX_API") {
t.Errorf("buildEnv() = %v, missing ENOWX_API", env)
}
})

t.Run("env:full inherits the parent environment", func(t *testing.T) {
man := &Manifest{ID: "demo", Permissions: []string{"env:full"}}
env := buildEnv(man, "demo", 5555, 1430)

if !hasEnvKey(env, "ENOWX_TEST_SECRET") {
t.Errorf("buildEnv() with env:full did not inherit ENOWX_TEST_SECRET")
}
})
}
12 changes: 6 additions & 6 deletions core/plugins/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ type Manifest struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"` // lucide name, or "icon.png" in the folder
Runtime string `json:"runtime"` // go | python | node | static
Entry string `json:"entry"` // command entry (ignored for static)
UI string `json:"ui"` // path served at /plugins/<id>/ (default public/index.html)
Version string `json:"version"` // semver, bumped on each marketplace re-publish
Permissions []string `json:"permissions,omitempty"`
Icon string `json:"icon"` // lucide name, or "icon.png" in the folder
Runtime string `json:"runtime"` // go | python | node | static
Entry string `json:"entry"` // command entry (ignored for static)
UI string `json:"ui"` // path served at /plugins/<id>/ (default public/index.html)
Version string `json:"version"` // semver, bumped on each marketplace re-publish
Permissions []string `json:"permissions,omitempty"` // "env:full" inherits the parent process env; default is a minimal allow-list (plugins.envAllowList)
}

var idRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,47}$`)
Expand Down