Skip to content

fix(approval): apply saved deny rules to safe shell commands and bash -c bodies - #1138

Draft
sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1332-deny-rules-safe-shell
Draft

sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1332-deny-rules-safe-shell

Conversation

@sosidudku1

@sosidudku1 sosidudku1 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What

Several gaps let a saved deny rule be skipped:

  1. Commands the shell guard deems safe (echo, ls) never reached the approval gate, so a deny rule on them did not apply.
  2. Bodies of bash -c "…" were not split into segments, so a deny rule on rm did not stop bash -c "rm -rf tmp".
  3. Wrappers with value-taking flags (timeout 5, sudo -u root, nice -n 5, env -u X), eval, xargs, find -exec, and a script piped into a bare shell (printf 'rm -rf tmp' | sh) hid the real command.

Now:

  • Safe commands. os.shell.run asks the gate's rule decision for a safe command too. A deny goes to the gate, which refuses it on the rule before any level, grant or prompt (level 5 included).
  • Nested shells. Rule segments also list the commands a nested bash/sh/zsh/dash/ksh/fish/csh/tcsh line runs: -c alone or bundled (-lc, -ec), --command, fish -C/--init-command, +x/+e options and +o/+O values, recursively up to 4 levels.
  • Wrappers. Unwrapped with a per-wrapper flag table (sudo, nice, timeout, env incl. VAR=val and -S, xargs, find -exec); eval joins its words and re-splits them. The shell list and the wrapper table now live once in command-segments.ts and are shared with the browser-bypass guard (identical tables, same verdicts).
  • Script fed on stdin. When a subshell line pipes or redirects into a shell or interpreter (sh, bash, python*, node, ruby, perl, php, osascript) that has no script of its own, the rule check cannot read what runs. Such a line is matched by any command deny or ask rule, so it is refused (or asked) when at least one exists. The refusal says why. With no rules, or only allow rules, it runs as before.

Allow rules still match only the top-level command. Ask rules still do not prompt on safe commands.

Why

"Never allow X" has to mean never: past wrappers, nested shells, eval and find -exec, and a script piped into a shell is refused under a deny rule.

Trade-offs

  • The stdin check is strict: with any command deny or ask rule saved, heredoc scripts such as python3 - <<'EOF' and curl … | bash are refused (deny) or asked (ask), even if the rule names an unrelated command.
  • Because a shell is looked for anywhere in a segment, echo bash -c "rm x" is also refused by an rm deny rule.
  • The guard's dangerous layer now uses the shared scanner, so fish/csh/tcsh -c and bash +x -c are now dangerous.shell_dash_c too (stricter).
  • The interpreter list and inline-code flag check are heuristics (bun/deno on stdin are not covered).

Tests

  • shell-saved-deny-rules.test.ts (level 5, tmp survives, nobody is asked): echo hi and ls ./ under their own deny rules; bash -c, sh -lc, tcsh -c, csh -c, fish -c, bash +x -c, sh +e -c; timeout 5, timeout -s KILL 5, sudo -u root, nice -n 5, env -u X, eval rm -rf tmp, echo tmp | xargs rm -rf, printf 'rm -rf tmp' | sh; an unrelated deny still runs echo hi; ask on echo does not prompt; allow on rm does not settle the nested form; no rules → piped scripts run as before.
  • shell-command-argv.test.ts: each unwrap, and what the stdin check flags and does not flag.
  • approval-rules.test.ts: an unreadable request is matched by deny and ask rules, not by allow.

Fixes ATO-1332.

Not yet checked on the second Mac.

…ed shell -c lines

A command the shell guard deems safe (echo, ls) never reached the
approval gate, so a saved deny rule on it did not apply. os.shell.run now
asks the gate's rule decision for such a command and sends a deny to the
gate, which refuses it before any level, grant or prompt. Ask and allow
rules keep their current semantics.

shellRuleSegments now also lists the commands a nested bash/sh/zsh -c
line runs (bundled flags like -lc, -ec, recursively, bounded depth), so
a deny rule on rm stops bash -c "rm -rf tmp". The nested-shell option
scan is shared with the guard's dangerous layer.

Refs ATO-1332
…ted shells

Saved deny rules now also see the command behind:
- csh, tcsh and fish -c (fish -C too), and + options (+x, +o pipefail)
  before -c; the nested-shell list and the wrapper flag table are shared
  with the guard's browser-bypass layer
- value-taking wrapper flags (sudo -u root, nice -n 5, timeout 5 /
  -s KILL, env -u X / -S, xargs -n 1)
- eval and find -exec

A script fed to a bare shell or interpreter through a pipe, a redirect
or a heredoc (printf ... | sh, sh < run.sh, curl ... | python3) cannot
be read, so the request is marked commandUnreadable and any deny or ask
command rule matches it; the refusal says why. Allow rules are unchanged.

Refs ATO-1332

This branch has not been deployed

No deployments
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.

1 participant