diff --git a/webview-ui/src/utils/__tests__/command-validation.spec.ts b/webview-ui/src/utils/__tests__/command-validation.spec.ts index 58edc86723..2427fc64b2 100644 --- a/webview-ui/src/utils/__tests__/command-validation.spec.ts +++ b/webview-ui/src/utils/__tests__/command-validation.spec.ts @@ -43,6 +43,12 @@ describe("Command Validation", () => { expect(parseCommand("diff <(sort f1) <(sort f2)")).toEqual(["diff", "sort f1", "sort f2"]) }) + it("handles nested backticks with escaped inner backticks", () => { + const cmd = "echo `echo \\`whoami\\``" + // Should surface both the outer echo and the inner whoami as separate sub-commands + expect(parseCommand(cmd)).toEqual(["echo", "echo", "whoami"]) + }) + it("handles empty and whitespace input", () => { expect(parseCommand("")).toEqual([]) expect(parseCommand(" ")).toEqual([]) @@ -1075,6 +1081,11 @@ describe("Unified Command Decision Functions", () => { // Main command with denied prefix should also be auto-denied expect(getCommandDecision("npm test $(echo hello)", allowedCommands, deniedCommands)).toBe("auto_deny") + + // Nested backticks with escaped inner backticks should not be auto-approved when inner command isn't allowed + expect(getCommandDecision("echo `echo \\`whoami\\``", ["echo"], [])).toBe("ask_user") + // And should be auto-denied when inner command is on the denylist + expect(getCommandDecision("echo `echo \\`whoami\\``", ["echo"], ["whoami"])).toBe("auto_deny") }) it("properly validates subshell commands when no denylist is present", () => { diff --git a/webview-ui/src/utils/command-validation.ts b/webview-ui/src/utils/command-validation.ts index 3a35b02849..a4c4f8c107 100644 --- a/webview-ui/src/utils/command-validation.ts +++ b/webview-ui/src/utils/command-validation.ts @@ -256,8 +256,10 @@ function parseCommandLine(command: string): string[] { subshells.push(inner.trim()) return `__SUBSH_${subshells.length - 1}__` }) - .replace(/`(.*?)`/g, (_, inner) => { - subshells.push(inner.trim()) + // Handle backticks with support for escaped backticks (e.g., \`) + .replace(/`((?:\\`|[^`])*)`/g, (_, inner) => { + const unescaped = inner.replace(/\\`/g, "`").trim() + subshells.push(unescaped) return `__SUBSH_${subshells.length - 1}__` }) @@ -318,7 +320,14 @@ function parseCommandLine(command: string): string[] { commands.push(currentCommand.join(" ")) currentCommand = [] } - commands.push(subshells[parseInt(subshellMatch[1])]) + // Expand subshell into its constituent commands to catch nested substitutions + const subshellContent = subshells[parseInt(subshellMatch[1])] + const expanded = parseCommand(subshellContent) + if (expanded.length > 0) { + commands.push(...expanded) + } else if (subshellContent.trim()) { + commands.push(subshellContent.trim()) + } } else { currentCommand.push(token) }