Skip to content

security: restrict unattended Layered Intelligence cmd sources to read-only binaries (#5669) - #5806

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-5669
Sep 2, 2026
Merged

security: restrict unattended Layered Intelligence cmd sources to read-only binaries (#5669)#5806
atomantic merged 2 commits into
mainfrom
claim/issue-5669

Conversation

@atomantic

@atomantic atomantic commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

A cmd custom source in an app's Layered Intelligence config runs on the autonomous Engine-B schedule, with the PortOS process's own privileges and nobody watching. Its only defense was validateCommand — the allowlist built for the manual, operator-triggered command runner, which admits npx, node, python, pip, curl, wget, go, cargo, make and brew. None of those needs a shell metacharacter to fetch and execute arbitrary code (npx <pkg>, pip install <pkg>, curl -o <path> <url>), so the metacharacter filter was doing all the work and the binary allowlist almost none — for the one lane whose whole point is that no human is in the loop.

This splits the two lanes:

  • New UNATTENDED_READONLY_COMMANDS (server/lib/commandSecurity.js) — git, gh, glab, ls, cat, head, tail, grep, find, wc, pwd, echo. Read-only repository/tracker inspection is the entire documented purpose of a cmd source (git log … | head is the example the escape hatch itself cites), so this costs no real capability while removing every network-fetch and code-execution verb.
  • New validateUnattendedCommand(cmd) beside validateCommand. Both now route through one private validateAgainst(command, allowlist, sorted), so the two gates can never disagree about parsing or about shell metacharacters — only the allowlist differs. The pm2 sub-check stays on the operator path only (pm2 is not on the new list at all).
  • runShellCommand (server/services/layeredIntelligence/sources.js) calls the new validator, and its warning names the narrower list.
  • THREAT MODEL comment rewritten to state the guarantee accurately, including why the operator-facing allowlist is the wrong gate here.

Unchanged on purpose: ALLOWED_COMMANDS, DANGEROUS_SHELL_CHARS, redactOutput, server/routes/commands.js, and the install-wide, off-by-default settings.layeredIntelligence.trustShellSources escape hatch — an operator who needs a pipeline or a broader binary still has it.

The follow-up commit records honest scope after local review: the gate is binary-level, not subcommand-level, so multi-purpose binaries (git commit, find -delete, gh api -X POST) still pass. That is a deliberately smaller step than subcommand gating — it removes the remote-code fetch/exec class, which is what turns hostile persistent config into arbitrary RCE. Subcommand gating is tracked in #5808.

Test plan

  • server/lib/commandSecurity.test.js — new validateUnattendedCommand block: rejects npx some-package, curl … -o /tmp/x, pip install evil, node, python, wget, brew, make, npm, pm2 (the regressions this uniquely catches, none of which carry a metacharacter); accepts git log --oneline -20, gh pr list, glab mr list, cat, head, grep, wc, pwd; still rejects metacharacters and blank input; asserts arg-parsing parity with validateCommand; and pins that validateCommand still accepts npx vitest / curl / pip install so the operator-facing runner is byte-identical.
  • server/services/layeredIntelligence.test.js — pins the wiring, not just the predicate: runShellCommand drops each operator-allowlisted-but-code-executing cmd source without spawning and warns about read-only inspection commands, and still runs npx some-package with shell: true when trustShellSources is on.
  • cd server && npm test1834 files passed / 1 skipped, 37314 tests passed.
  • Local codex review ran (1 round, gpt-5.6-terra, read-only sandbox); its one substantive finding is addressed by the scope commit above.

Closes #5669

https://claude.ai/code/session_01GMxEz43s3YCLaVZV9KmVwE

…d-only binaries (#5669)

A `cmd` custom source in an app's Layered Intelligence config is executed on
the autonomous Engine-B schedule, with the PortOS process's privileges and no
human watching. Its only defense was `validateCommand` — the allowlist built
for the manual, operator-triggered command runner, which admits `npx`, `node`,
`python`, `pip`, `curl`, `wget`, `go`, `cargo`, `make` and `brew`. None of
those need a shell metacharacter to fetch and run arbitrary code (`npx <pkg>`,
`pip install <pkg>`, `curl -o <path> <url>`), so the metacharacter filter was
doing all the work and the binary allowlist almost none.

The unattended lane now uses its own `UNATTENDED_READONLY_COMMANDS` allowlist —
`git`, `gh`, `glab`, `ls`, `cat`, `head`, `tail`, `grep`, `find`, `wc`, `pwd`,
`echo` — which covers the documented purpose of a `cmd` source (read-only
repository and tracker inspection) while removing every network-fetch and
code-execution verb. Both validators share one parse + metacharacter body so
the two gates can never disagree about anything but the allowlist.

The operator-facing runner (`POST /api/commands/execute`) is unchanged, and the
install-wide, off-by-default `settings.layeredIntelligence.trustShellSources`
opt-in still restores full-shell behavior for operators who need a pipeline.

Claude-Session: https://claude.ai/code/session_01GMxEz43s3YCLaVZV9KmVwE
Local review flagged that "read-only" overstates the gate: it admits binaries,
not subcommands, so multi-purpose ones (`git commit`, `find -delete`,
`gh api -X POST`) still pass. Record that scope in the allowlist comment rather
than let the name imply more, and switch the parse-parity test example from
`git commit` to `git log --grep` so it stops reading as an endorsement.

No behavior change. Subcommand gating is tracked as follow-up work.

Claude-Session: https://claude.ai/code/session_01GMxEz43s3YCLaVZV9KmVwE
@atomantic
atomantic merged commit 2948fa0 into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5669 branch September 2, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use a narrower binary allowlist for unattended Layered Intelligence cmd sources than for the operator-driven command runner

1 participant