Skip to content

Commit de2c626

Browse files
committed
fix: improve subshell detection to avoid false positives with markdown content
1 parent 2b8228e commit de2c626

File tree

1 file changed

+39
-4
lines changed

1 file changed

+39
-4
lines changed

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

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ export function parseCommand(command: string): string[] {
123123
currentCommand.push(token.op)
124124
}
125125
} else if (typeof token === "string") {
126-
// Check if it's a subshell placeholder
126+
// Check if this is a subshell placeholder
127127
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
128128
if (subshellMatch) {
129+
// Add the subshell command as a separate command
129130
if (currentCommand.length > 0) {
130131
commands.push(currentCommand.join(" "))
131132
currentCommand = []
@@ -279,6 +280,40 @@ export function isAutoDeniedSingleCommand(
279280
return longestDeniedMatch.length >= longestAllowedMatch.length
280281
}
281282

283+
/**
284+
* Check if a command contains subshell execution that should be blocked.
285+
* Only blocks if there's a denylist configured and the command isn't just outputting text.
286+
*/
287+
function containsBlockableSubshell(command: string, deniedCommands?: string[]): boolean {
288+
if (!deniedCommands?.length) return false
289+
290+
const trimmedCommand = command.trim()
291+
const isTextOutputCommand =
292+
/^(echo|printf|cat|print)\s+["']/.test(trimmedCommand) || /^(echo|printf|cat|print)\s+\\?"/.test(trimmedCommand)
293+
294+
if (isTextOutputCommand) return false
295+
296+
// Look for actual command substitution $()
297+
const hasCommandSubstitution = /\$\([^)]+\)/.test(command)
298+
299+
// For backticks, be more careful - they could be in markdown
300+
let hasBacktickSubstitution = false
301+
if (command.includes("`")) {
302+
// Simple heuristic: if the command has multi-line content or markdown-like
303+
// patterns, the backticks are probably not for command substitution
304+
const hasMarkdownIndicators =
305+
command.includes("```") || command.includes("\n") || command.includes("##") || command.includes("**")
306+
307+
if (!hasMarkdownIndicators) {
308+
// Check if backticks are likely command substitution
309+
// Look for patterns like: cmd `subcmd` or var=`cmd`
310+
hasBacktickSubstitution = /[^\\]`[^`\n]+`/.test(command)
311+
}
312+
}
313+
314+
return hasCommandSubstitution || hasBacktickSubstitution
315+
}
316+
282317
/**
283318
* Check if a command string should be auto-approved.
284319
* Only blocks subshell attempts if there's a denylist configured.
@@ -288,7 +323,7 @@ export function isAutoApprovedCommand(command: string, allowedCommands: string[]
288323
if (!command?.trim()) return true
289324

290325
// Only block subshell execution attempts if there's a denylist configured
291-
if ((command.includes("$(") || command.includes("`")) && deniedCommands?.length) {
326+
if (containsBlockableSubshell(command, deniedCommands)) {
292327
return false
293328
}
294329

@@ -313,7 +348,7 @@ export function isAutoDeniedCommand(command: string, allowedCommands: string[],
313348
if (!command?.trim()) return false
314349

315350
// Only block subshell execution attempts if there's a denylist configured
316-
if ((command.includes("$(") || command.includes("`")) && deniedCommands?.length) {
351+
if (containsBlockableSubshell(command, deniedCommands)) {
317352
return true
318353
}
319354

@@ -385,7 +420,7 @@ export function getCommandDecision(
385420
if (!command?.trim()) return "auto_approve"
386421

387422
// Only block subshell execution attempts if there's a denylist configured
388-
if ((command.includes("$(") || command.includes("`")) && deniedCommands?.length) {
423+
if (containsBlockableSubshell(command, deniedCommands)) {
389424
return "auto_deny"
390425
}
391426

0 commit comments

Comments
 (0)