mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
fix(security): remove startup secrets from the environment before child processes spawn
ARTEX_PG_DSN, ANTHROPIC_API_KEY and OPENAI_API_KEY are read only during startup (server.NewManager and agent.FromEnv in server.New), but stayed in the process environment. Agent shells, custom tools and MCP stdio servers copy os.Environ() when they start, so they could read the database password and the LLM keys. main now unsets the three variables right after server.New returns.
This commit is contained in:
@@ -94,6 +94,7 @@ func run() int {
|
||||
}
|
||||
log.Printf("[config] skill directory: %s", skillDir)
|
||||
srv := server.New(ctx, mgr, skillDir, *dataDir, config.BaseDir())
|
||||
scrubSecretEnv()
|
||||
httpSrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: srv.Handler(),
|
||||
@@ -117,6 +118,19 @@ func run() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// secretEnv lists the credentials read from the environment only at startup:
|
||||
// ARTEX_PG_DSN by server.NewManager, the API keys by agent.FromEnv in server.New.
|
||||
var secretEnv = []string{"ARTEX_PG_DSN", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"}
|
||||
|
||||
// scrubSecretEnv removes secretEnv from the process environment once startup has
|
||||
// read it. Child processes (agent shells, custom tools, MCP stdio servers) copy
|
||||
// os.Environ() when they are spawned, so they no longer inherit these values.
|
||||
func scrubSecretEnv() {
|
||||
for _, k := range secretEnv {
|
||||
_ = os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdownContext deliberately does not derive from signalCtx. If it did, the
|
||||
// parent's plain context.Canceled could win the race before AbortShutdown was
|
||||
// attached to the child, losing the diagnostic cause in every running Agent.
|
||||
|
||||
@@ -2,11 +2,42 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/agent"
|
||||
)
|
||||
|
||||
// A child process spawned after startup must not see the startup secrets, while
|
||||
// other variables still pass through.
|
||||
func TestScrubSecretEnvHidesSecretsFromChildren(t *testing.T) {
|
||||
sh, err := exec.LookPath("sh")
|
||||
if err != nil {
|
||||
t.Skip("no sh in PATH")
|
||||
}
|
||||
for _, k := range secretEnv {
|
||||
t.Setenv(k, "leak-"+k)
|
||||
}
|
||||
t.Setenv("ARTEX_SCRUB_KEEP", "kept")
|
||||
|
||||
scrubSecretEnv()
|
||||
|
||||
out, err := exec.Command(sh, "-c", "env").Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := "\n" + string(out)
|
||||
for _, k := range secretEnv {
|
||||
if strings.Contains(env, "\n"+k+"=") {
|
||||
t.Errorf("%s still reaches child processes", k)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(env, "\nARTEX_SCRUB_KEEP=kept\n") {
|
||||
t.Errorf("non-secret variable did not reach the child process")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShutdownContextPreservesNamedCause(t *testing.T) {
|
||||
signalCtx, signalCancel := context.WithCancel(context.Background())
|
||||
ctx, shutdown := shutdownContext(signalCtx)
|
||||
|
||||
Reference in New Issue
Block a user