Merge pull request #11890 from Kilo-Org/mark/readonly-bash-exec-flag-denies

fix(cli): close read-only bash exec-flag escapes
This commit is contained in:
Mark IJbema
2026-07-02 16:55:51 +02:00
committed by GitHub
3 changed files with 36 additions and 8 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Close read-only bash escapes where allowed commands could still run arbitrary programs via flags (`sort --compress-program`, `rg --pre`, `ag --pager`, `man -P`/`-H`).
+21 -8
View File
@@ -109,24 +109,37 @@ export const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
"git branch -r *": "allow",
"git remote -v *": "allow",
"gh *": "ask",
// Everything below is a blocklist layered on the allowlist above: it catches ways
// an "allowed" read-only command can still write files, chain commands, or exec an
// arbitrary program. This is defense-in-depth, not a sandbox — the durable fix is
// OS-level sandboxing, not command-line string matching.
// `*` matches any run of characters (including spaces and empty), so each rule
// catches its operator anywhere. Broad forms subsume narrow ones: `*&*` covers
// `&&`, and `*>*` covers `>`, `>>`, `>|`, and `>(` in any spacing.
"*\n*": "deny",
"*<(*": "deny",
"*|*": "deny",
"*;*": "deny",
"*&&*": "deny",
"*&*": "deny",
"*$(*": "deny",
"*`*": "deny",
"*>*": "deny",
"* > *": "deny",
"*>>*": "deny",
"* >> *": "deny",
"*>|*": "deny",
"* >| *": "deny",
// Short -o is space-anchored (two forms) so it never matches filenames like
// `foo-o bar`; long flags use `*--flag*`, which is specific enough to bridge both
// "flag first" and "flag after args" positions in one rule.
"sort -o *": "deny",
"sort * -o *": "deny",
"sort --output*": "deny",
"sort * --output*": "deny",
"sort *--output*": "deny",
// Flags that make otherwise "read-only" commands exec an arbitrary program.
"sort *--compress-program*": "deny",
"sort *--files0-from*": "deny",
"rg *--pre *": "deny",
"rg *--pre=*": "deny",
"rg *--hostname-bin*": "deny",
"ag *--pager*": "deny",
"man *-P*": "deny",
"man *--pager*": "deny",
"man *-H*": "deny",
}
function askGuard(mcp: Record<string, "allow" | "ask" | "deny"> = {}) {
@@ -121,6 +121,16 @@ describe("Ask agent bash permissions", () => {
"sort names.txt --output=names.txt",
"echo ok\ntouch ask-bypass.txt",
"cat <(touch ask-bypass.txt)",
// Exec-via-flag escapes on otherwise read-only commands
'sort -S 1b --compress-program "sh" names.txt',
"sort --compress-program=sh names.txt",
"sort --files0-from=list names.txt",
"rg --pre sh -e . names.txt",
"rg --pre=sh -e . names.txt",
"ag --pager sh foo",
"man -P sh ls",
"man -Psh ls",
"man --pager=sh ls",
]
for (const cmd of denied) {