fix(approval): catch sudo with stdin/askpass/shell privilege flags

Adds the only #17873 category not covered by the in-flight PRs #17962
(briandevans, reverse shell + download-execute) and #7993 (SHL0MS,
credential reads + curl/wget exfiltration): sudo invocations that an
LLM-driven agent can drive without TTY interaction.

The agent has no TTY, so the sudo forms that succeed without human
involvement are those reading the password from stdin (`-S` / `--stdin`)
or via an askpass helper (`-A` / `--askpass`). The shell-launch (`-s`)
and list-privileges (`-a`) flags are also gated since they are
privilege-relevant invocations the agent can chain after acquiring the
password (e.g. read SUDO_PASSWORD from .env -> sudo -S -s -> root shell).
Plain `sudo cmd` (no flag) is TTY-bound and excluded.

Two patterns:

  1. Direct flag: `\bsudo\b[^;|&\n]*?\s+(?:-s\b|--stdin\b|-a\b|--askpass\b)`
     The lazy `[^;|&\n]*?` consumes flag-arguments without spanning
     command separators, so `sudo -u root -S whoami` matches (a textbook
     offensive form that a strict `(?:\s+-[^\s]+)*` "leading flags only"
     pattern would have missed because `root` is a flag-value not a flag).

  2. Combined short flags: `\bsudo\b[^;|&\n]*?\s+-[a-z]*[sa][a-z]*\b`
     Catches packed forms like `sudo -nS id` where multiple flags share
     a single `-X` token.

`_normalize_command_for_detection` lowercases input before pattern
matching (tools/approval.py:340), so case variants of S/s and A/a
collapse — both letter-pairs are gated since each is a privilege-
relevant invocation.

Tests: 21 new cases in TestDetectSudoStdin (12 positive covering all
flag-order permutations including herestring source and printf-piped
forms; 9 negative including TTY-bound `sudo whoami`, interactive
`sudo -i`, env-var reference `$SUDO_USER`, doc lookup `man sudo`,
package install, and the `pseudosudo` word-boundary edge case).

Empirical coverage: 11/11 attacks matched, 0/10 false positives.

Refs: #17873 category 4. Adjacent: #17962 (reverse shell + download-
execute), #7993 (credential reads + curl/wget exfiltration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
fr33d3m0n 2026-05-07 17:36:39 +08:00 committed by kshitij
parent 9520a1ccdf
commit 976d8e27ad
2 changed files with 156 additions and 0 deletions

View file

@ -368,6 +368,25 @@ DANGEROUS_PATTERNS = [
# a script is first made executable then immediately run. The script
# content may contain dangerous commands that individual patterns miss.
(r'\bchmod\s+\+x\b.*[;&|]+\s*\./', "chmod +x followed by immediate execution"),
# Sudo with stdin / askpass / shell / list-privs flags. An LLM-driven
# agent has no TTY, so sudo invocations that succeed without human
# interaction are those reading the password from stdin (-S/--stdin)
# or via an askpass helper (-A/--askpass). The shell-launch (-s) and
# list-privileges (-a) flags are also gated since they are
# privilege-relevant invocations the agent can chain after acquiring
# the password (e.g. read SUDO_PASSWORD from .env -> sudo -S -s ->
# root shell). Plain `sudo cmd` (no flag) is TTY-bound and excluded.
# `_normalize_command_for_detection` lowercases input before pattern
# matching, so case variants of S/s and A/a collapse — both forms
# are gated below. Lazy `[^;|&\n]*?` allows flag arguments (e.g.
# `sudo -u root -S whoami`) without spanning command separators. See
# #17873 category 4.
(r'\bsudo\b[^;|&\n]*?\s+(?:-s\b|--stdin\b|-a\b|--askpass\b)',
"sudo with privilege flag (stdin/askpass/shell/list)"),
# Combined short-flag form: -nS, -ns, -sa, -las — sudo flags packed
# into a single -X token. Catches the same threat class.
(r'\bsudo\b[^;|&\n]*?\s+-[a-z]*[sa][a-z]*\b',
"sudo with combined-flag privilege escalation"),
]