GHSA-jxh8-jh77-xh6g: @evomap/evolver's validator sandbox allowlist permits `npm`/`npx`, yielding RCE from Hub-delivered validation tasks via lifecycle scripts
SummaryThe validator-mode sandbox executor (`src/gep/validator/sandboxExecutor.js`) places `npm` and `npx` in its hard executable allowlist. Because `npm install <pkg>` and `npx -y -p <pkg> <bin>` execute arbitrary code by design (preinstall/install/postinstall lifecycle scripts and remote-package bin entries), and because validator nodes consume `validation_commands` strings from unsigned Hub responses with no per-response signature check, an attacker who controls or MITMs the Hub achieves automatic remote code execution on every validator node within one daemon poll (default 60s).DetailsEnd-to-end chain:`src/gep/validator/index.js:71-87` — `fetchValidationTasks()` POSTs to `<hub>/a2a/fetch` and reads `validation_tasks` from the JSON response. The outbound request is signed via `buildHubHeaders()`, but the Hub's response is parsed directly with `await res.json()` and no signature is verified on `data.payload`.`src/gep/validator/index.js:98-108` — `validateOneTask()` extracts `task.validation_commands` (an array of attacker-controlled strings) and passes it straight to `runInSandbox(commands, {})`. No call to `policyCheck.isValidationCommandAllowed()` happens on this path. The author's own comment at `sandboxExecutor.js:41-42` acknowledges this gap: *"This closes the gap where validation_commands go straight from Hub to runInSandbox without passing through policyCheck.isValidationCommandAllowed()."*`src/gep/validator/sandboxExecutor.js:172-218` — `runSingleCommand` calls `parseCommand(cmd)`, then checks `ALLOWED_EXECUTABLES.has(parsed.executable)`:`parseCommand` only rejects shell metacharacters (`| & ; > < \` $`) and unbalanced quotes. A string like `npm install /tmp/evil-pkg --no-audit --no-fund` contains none of those and parses cleanly into `{ executable: 'npm', args: [...] }`.`sandboxExecutor.js:54-66` — `assertNodeCommandSafe` is a no-op for non-`node` executables:The `BLOCKED_NODE_FLAGS` set (`-e`, `-r`, `--loader`, etc.) therefore never gates `npm` or `npx` invocations.`sandboxExecutor.js:213` — `spawn('npm', [...], { shell: false, cwd: sandboxDir, env })` runs `npm`. npm's documented behavior is to execute the package's `preinstall`, `install`, and `postinstall` scripts; `npx` downloads a remote package and executes its `bin` entry. Both yield arbitrary code execution in the validator process's UID/permissions.`src/gep/validator/index.js:189` — the validator daemon polls every 60s by default (`EVOLVER_VALIDATOR_DAEMON_INTERVAL_MS`), and validator mode is **on by default** since v1.69.0 (`isValidatorEnabled()` returns `true` unless explicitly disabled, `index.js:25-34`).The "sandbox" is nominal: it sets a fresh `cwd` and a stripped env (HOME → tmpdir to hide `~/.npmrc`/`~/.ssh`), but `PATH` is preserved (so `npm`/`npx` resolve), there is no container/chroot/seccomp/uid drop, and nothing prevents the spawned process from writing arbitrary files, opening outbound connections, or reading any file readable by the validator process.The author's documented threat model at `sandboxExecutor.js:31-34` explicitly includes Hub compromise:"Any command whose first token is not in this set is rejected before spawn(). This prevents command injection via Hub-delivered task.command strings even if Hub itself is compromised or mis-signs a task."Putting `npm` and `npx` on that allowlist defeats that stated goal — both are arbitrary-code-execution-by-design tools.PoCReproduced against v1.70.0-beta.4 (HEAD on `main`):Step 1 — plant a malicious package locally (the remote-tarball variant works identically; npm fetches and runs lifecycle scripts in both cases):Step 2 — invoke the exact code path used by `validateOneTask()` when the Hub returns a task with `validation_commands: ["npm install /tmp/evil-pkg-validator --no-audit --no-fund"]`:Observed output (verified):The sandbox reports `overallOk: true` (it sees a clean exit-0 from `npm`), while the preinstall script has already written `/tmp/pwned-by-validator-test` outside the sandbox directory — uncontained code execution as the validator UID.Remote-only variant (no local file required): a compromised or MITM'd Hub returns:orBoth pass `parseCommand()` (no shell metacharacters), pass `ALLOWED_EXECUTABLES.has('npm'|'npx')`, and `assertNodeCommandSafe` is a no-op for them. npm/npx fetch the remote tarball and execute its lifecycle/bin scripts on the validator host.Impact**Arbitrary code execution** as the evolver/validator process UID on every validator node that polls the malicious Hub (one cycle ≈ 60s by default).**Credential exfiltration**: HUB_NODE_SECRET, A2A node identity, any cloud/cred material readable by the process.**Persistence / lateral movement**: write to user-writable cron, systemd-user units, shell rc files; pivot into the host's container / VM.**Wormable across the network**: a single Hub compromise auto-RCEs every node running validator mode — and validator mode is opt-out / on by default since v1.69.0.**Defeats the documented sandbox guarantee**: the executor advertises defense against a compromised Hub; in practice, two of its three allowed binaries are arbitrary-code-execution tools.Recommended FixRemove `npm` and `npx` from `ALLOWED_EXECUTABLES`. Validation tasks need only `node <script>`:If `npm test` / `npx vitest` style commands must remain reachable from the Hub path, harden them explicitly:Additionally:**Sign the Hub's `/a2a/fetch` *response*** the same way outbound requests are signed (`buildHubHeaders`). Verify the signature on `data.payload` in `fetchValidationTasks` before handing tasks to `runInSandbox`. This closes the network-MITM variant that does not require Hub compromise.**Run `runInSandbox` under real isolation** — drop privileges, disable network, mount tmpfs, apply seccomp — rather than relying solely on an allowlist. The current `buildSandboxEnv` only redirects `HOME`/`TMPDIR`; the spawned process otherwise has full host access.**Apply `policyCheck.isValidationCommandAllowed()` to Hub-delivered `validation_commands`** in `validateOneTask`, mirroring the gate that already exists for capsule-derived commands in `solidify.js` / `skill2gep.js`.
CVSS v3.1: 8.1