Skip to content

fix: gate unattended commands at the subcommand level, not just the binary (#5808) - #5997

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-5808
Sep 3, 2026
Merged

fix: gate unattended commands at the subcommand level, not just the binary (#5808)#5997
atomantic merged 1 commit into
mainfrom
claim/issue-5808

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

The unattended Layered Intelligence cmd lane runs command strings out of persistent, attacker-reachable config on a schedule with nobody watching. #5806 narrowed it to a read-only binary allowlist, which closed the remote-code-fetch/exec class — but the gate was binary-level, and four admitted binaries are multi-purpose. git reset --hard, git checkout ., git clean -fd, find . -delete, find . -exec …, gh api -X POST … and gh pr merge all contain no shell metacharacter, so both the allowlist and the metacharacter filter waved them straight through — destroying uncommitted work in the app repo, deleting arbitrary files, running arbitrary binaries (find -exec re-opened execution entirely), or writing to the tracker with the operator's own credentials.

validateUnattendedCommand now applies a per-binary subcommand check after the allowlist, keyed off baseCommand the same way validateCommand already keys its pm2 check — the shape matches the existing precedent rather than introducing a second mechanism.

  • git — an allowlist (not a denylist; git grows verbs) of read-only subcommands: log, show, status, diff, blame, describe, rev-parse, branch, tag, remote, config, ls-files, shortlog. Unknown verbs fail closed. The three listing verbs keep only their listing forms, so git branch -D main, git branch <new>, git tag v1, git remote add, and a git config <key> <value> write are all rejected while git branch -a, git tag -l v1, git remote -v, git remote show origin and git config --get user.name still pass.
  • find — the action flags (-delete, -exec, -execdir, -ok, -okdir, -fls, -fprint, -fprint0, -fprintf) are rejected anywhere in the args. Inspection predicates (-name, -type, -maxdepth, -print) are untouched.
  • gh / glab — only <noun> list, <noun> view, and api limited to GET. Field flags (-f, -F, --field, --raw-field, --input) are rejected too, because they make api an implicit POST even with no -X.

ls, cat, head, tail, grep, wc, pwd and echo have no write mode and deliberately stay binary-level.

Scope held to the issue: ALLOWED_COMMANDS, DANGEROUS_SHELL_CHARS, redactOutput and server/routes/commands.js are untouched; no binaries were added to UNATTENDED_READONLY_COMMANDS; the operator-driven runner (validateCommand / POST /api/commands/execute) is byte-identical, since a human triggers and watches it and git commit there is legitimate; and the install-level settings.layeredIntelligence.trustShellSources escape hatch still restores full-shell behavior unchanged. The UNATTENDED_READONLY_COMMANDS scope comment and the commandSecurity.js row in server/lib/README.md now point at the implementation instead of describing subcommand gating as tracked separately.

Test plan

  • cd server && npm test1888 test files passed, 1 skipped, 0 failed; 38145 tests passed.
  • New cases in server/lib/commandSecurity.test.js cover: the destructive git forms and an unknown-to-the-allowlist verb; the read-only git forms that must keep working; the mutating form of each listing verb; the find action flags (using the + terminator, since the {}/; form is already caught upstream by the metacharacter filter and would prove nothing new); gh/glab writes including --method=PATCH, an implicit-POST field flag, and a malformed methodless -X; the read-only tracker commands; and a pin that validateCommand('git commit …'), gh pr merge 1, git reset --hard and find . -delete all still pass on the operator lane.
  • Note for reviewers: the issue suggested git diff HEAD~1 as an accept case — that string is already rejected one layer earlier because ~ is in DANGEROUS_SHELL_CHARS, so the test uses git diff --stat and records why.

Closes #5808

…inary (#5808)

The Layered Intelligence `cmd` lane runs strings from persistent config on a
schedule with nobody watching. Its allowlist admitted whole binaries, and four
of them are multi-purpose: `git reset --hard`, `git clean -fd`, `find . -delete`,
`find . -exec`, `gh api -X POST` and `gh pr merge` all carry no shell
metacharacter, so both the allowlist and the metacharacter filter waved them
through — destroying uncommitted work, deleting files, running arbitrary
binaries, or writing to the tracker with the operator's own credentials.

`validateUnattendedCommand` now applies a per-binary subcommand check after the
allowlist, keyed off the base command the same way the existing pm2 check is:

- `git` — an allowlist of read-only verbs (log, show, status, diff, blame,
  describe, rev-parse, branch, tag, remote, config, ls-files, shortlog).
  Unknown verbs fail closed, and the three listing verbs are held to their
  listing forms (`git branch -D`, `git tag v1`, `git remote add`, and a
  `git config` write are all rejected).
- `find` — the action flags (`-delete`, `-exec`/`-execdir`/`-ok`/`-okdir`,
  the `-fprint` family) are rejected anywhere in the args; inspection
  predicates are untouched.
- `gh` / `glab` — only `<noun> list`, `<noun> view`, and `api` restricted to
  GET. Field flags are rejected too, since they make `api` an implicit POST.

`ls`, `cat`, `head`, `tail`, `grep`, `wc`, `pwd` and `echo` have no write mode
and stay binary-level. The operator-driven runner
(`validateCommand` / POST /api/commands/execute) is unchanged — a human
triggers and watches it — and the `trustShellSources` escape hatch still
restores full-shell behavior for an install that opts in.

Claude-Session: https://claude.ai/code/session_01Do7xpqX5RLHM78rnstRkzf
@atomantic
atomantic merged commit ba3bd9e into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5808 branch September 3, 2026 05:17
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.

Gate unattended cmd sources at the subcommand level, not just the binary (git reset / find -delete / gh api -X POST)

1 participant