security: restrict unattended Layered Intelligence cmd sources to read-only binaries (#5669) - #5806
Merged
Conversation
…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
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
cmdcustom 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 wasvalidateCommand— the allowlist built for the manual, operator-triggered command runner, which admitsnpx,node,python,pip,curl,wget,go,cargo,makeandbrew. 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:
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 acmdsource (git log … | headis the example the escape hatch itself cites), so this costs no real capability while removing every network-fetch and code-execution verb.validateUnattendedCommand(cmd)besidevalidateCommand. Both now route through one privatevalidateAgainst(command, allowlist, sorted), so the two gates can never disagree about parsing or about shell metacharacters — only the allowlist differs. Thepm2sub-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.Unchanged on purpose:
ALLOWED_COMMANDS,DANGEROUS_SHELL_CHARS,redactOutput,server/routes/commands.js, and the install-wide, off-by-defaultsettings.layeredIntelligence.trustShellSourcesescape 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— newvalidateUnattendedCommandblock: rejectsnpx 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); acceptsgit log --oneline -20,gh pr list,glab mr list,cat,head,grep,wc,pwd; still rejects metacharacters and blank input; asserts arg-parsing parity withvalidateCommand; and pins thatvalidateCommandstill acceptsnpx vitest/curl/pip installso the operator-facing runner is byte-identical.server/services/layeredIntelligence.test.js— pins the wiring, not just the predicate:runShellCommanddrops each operator-allowlisted-but-code-executingcmdsource without spawning and warns about read-only inspection commands, and still runsnpx some-packagewithshell: truewhentrustShellSourcesis on.cd server && npm test→ 1834 files passed / 1 skipped, 37314 tests passed.codexreview 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