Skip to content

fix(approval): sudo-S brute-force block + sudo-stdin/askpass DANGEROUS (salvage of #22194 + #21128) - #23736

Merged
kshitijk4poor merged 3 commits into
mainfrom
salvage/sudo-hardening-9590-17873
May 11, 2026
Merged

fix(approval): sudo-S brute-force block + sudo-stdin/askpass DANGEROUS (salvage of #22194 + #21128)#23736
kshitijk4poor merged 3 commits into
mainfrom
salvage/sudo-hardening-9590-17873

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Salvages two complementary security fixes for the sudo-password class of issues into one PR. Both target the same underlying problem: an LLM agent with no TTY can attempt sudo brute-forcing or unauthorized privilege escalation by piping passwords via sudo -S/--stdin/--askpass. The two PRs cover different layers of the defense:

Layer From What it does
Unconditional block #22194 by @29206394 Blocks sudo -S (the LLM brute-force vector) in check_all_command_guards above yolo, when SUDO_PASSWORD is unset. Cannot be bypassed by --yolo / approvals.mode=off.
Approval-required (DANGEROUS) #21128 by @fr33d3m0n Adds sudo -S/--stdin/-A/--askpass/-s/-a and combined-short-flag variants to DANGEROUS_PATTERNS. Surfaces the privilege-relevant invocations through normal approval (yolo can bypass — by design).

Together they address #9590 (sudo password chat-leak via small-model brute-force) and #17873 cat 4 (broader sudo privilege-flag surface).

Why both layers

The two layers don't overlap behaviorally. The unconditional guard fires only when SUDO_PASSWORD is unset; the legitimate Hermes _transform_sudo_command path (which injects -S itself when SUDO_PASSWORD is configured) is unchanged.

Closes / supersedes

Changes

File +lines What
tools/approval.py +78 _check_sudo_stdin_guard() + 2 DANGEROUS_PATTERNS entries
tests/tools/test_hardline_blocklist.py +88 6 tests for the unconditional guard (positive, allow, password-bypass, integration, yolo-can't-bypass, container bypass)
tests/tools/test_approval.py +137 9+ tests for the DANGEROUS_PATTERNS coverage (stdin, askpass, herestring, combined flags, printf form)
scripts/release.py +2 AUTHOR_MAP entries for both contributors

303 net lines, 254 tests pass in the modified suites (test_hardline_blocklist.py + test_approval.py).

E2E verification

Verified through check_all_command_guards() with real imports (no mocks) that:

Case Expected Actual
sudo -S whoami (SUDO_PASSWORD unset) BLOCKED unconditionally ✅ BLOCKED
`echo guess1 sudo -S whoami` (brute-force) BLOCKED
sudo -k && sudo -S whoami BLOCKED ✅ BLOCKED
sudo whoami (plain sudo) ALLOWED ✅ ALLOWED
ls -la ALLOWED ✅ ALLOWED
sudo -S whoami with HERMES_YOLO_MODE=1 BLOCKED (yolo can't bypass) ✅ BLOCKED
sudo -S whoami in docker backend ALLOWED (container isolated) ✅ ALLOWED
sudo --stdin id, sudo -A whoami, sudo -nS id, sudo -u root -S whoami, sudo -S id <<< 'pw', `printf "%s\n" "$PW" sudo -S id` flagged as DANGEROUS (approval required)

Test plan

# Modified test suites
bash scripts/run_tests.sh tests/tools/test_hardline_blocklist.py tests/tools/test_approval.py
# 254 passed, 0 failed

# Full tools/ directory: 11 pre-existing failures on origin/main
# (all in test_file_read_guards.py / test_file_staleness.py / test_file_state_registry.py
#  — macOS /var → /private/var symlink issue, unrelated to this PR)

Lint diff: ruff clean, no new ty issues on changed files.

Authorship

Cherry-picked individual commits to preserve per-commit authorship:

Both contributors credited via AUTHOR_MAP. Original PRs will be closed pointing to this one.

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: salvage/sudo-hardening-9590-17873 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8158 on HEAD, 8158 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4288 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/tools Tool registry, model_tools, toolsets tool/terminal Terminal execution and process management P1 High — major feature broken, no workaround labels May 11, 2026
OpenClaw Agent and others added 3 commits May 11, 2026 19:25
…not set

Fixes #9590: Block explicit sudo -S (stdin password mode) commands
when the SUDO_PASSWORD environment variable is not configured.

The attack vector: the LLM constructs 'echo guessedpass | sudo -S cmd'
to brute-force sudo passwords, iterates based on sudo's error output
('Sorry, try again').  The existing _transform_sudo_command only
injects -S when SUDO_PASSWORD exists; without it, the LLM's explicit
sudo -S must be treated as a guessing attempt.

Changes:
- Add _check_sudo_stdin_guard() in approval.py: detects sudo -S when
  SUDO_PASSWORD is absent, anchored to command-start positions
  (^ ; && || | etc.) to avoid false positives on literal text
- Integrate into check_all_command_guards() above yolo/mode=off so
  the block is unconditional (like the hardline floor)
- Add 6 tests covering: detection, allow-list, SUDO_PASSWORD bypass,
  integration with check_all_command_guards, yolo non-bypass,
  container backend bypass
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>
- openclaw@agent.local → 29206394 (PR #22194)
- freedemon@gmail.com  → fr33d3m0n (PR #21128)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P1 High — major feature broken, no workaround tool/terminal Terminal execution and process management type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Agent attempts to guess and display sudo password in chat

3 participants