Fix command injection via newline and quoting/wildcard blocklist bypass#429
Conversation
Addresses two critical security vulnerabilities in command validation: 1. Newline bypass (wonderwhy-er#422): The extractCommands() separator array did not include newline characters (\n, \r\n, \r). An attacker could chain a benign command with a blocked command using a newline, and the shell would execute both while validation only checked the first. 2. Quoting and wildcard bypass (wonderwhy-er#421): extractBaseCommand() did not strip surrounding quotes from command tokens, so "rm" would not match the blocked command rm. Additionally, glob characters (*, ?, [, ]) in command paths (e.g. /usr/bin/su*o) would evade literal blocklist matching but expand to the real binary at shell execution time. Changes: - Add \r\n, \n, \r to the command separators array - Strip single and double quotes from command tokens before comparison - Reject any command containing glob/wildcard characters in both extractBaseCommand() and validateCommand()
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
📝 WalkthroughWalkthroughextractCommands now treats CR/LF/newline as command separators; extractBaseCommand strips surrounding quotes, rejects tokens containing glob/brace characters by returning null (propagated as 'INVALID_COMMAND'); validateCommand enforces fail-closed behavior when invalid or glob-containing tokens are present. Changes
Sequence Diagram(s)Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
CodeAnt AI finished reviewing your PR. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/command-manager.ts`:
- Around line 221-225: The firstToken quote-stripping only removes a perfectly
wrapped pair and misses concatenated quoted/unquoted fragments (e.g. `"r"m`), so
before you perform the basename/blocklist check normalize quoting across the
whole token (or replace internal single/double quote characters) so `"r"m`,
`r"m"`, and `'r''m'` become rm, or alternatively parse the command with a
shell-aware tokenizer; apply this change where firstToken is prepared for the
basename/blocklist comparison so the blocklist check sees the actual command
name.
- Around line 248-252: The glob-wildcard rejection currently tests the entire
input string; change it to only test the resolved executable token by calling
getBaseCommand(command) (the same helper used at line 229) and run the
/[*?\[\]]/ test against that returned base command; if the base command contains
glob chars return false, otherwise allow the rest of the command to contain
quoted/escaped metacharacters. Ensure you preserve the existing regex and return
behavior but switch the input from the full command string to
getBaseCommand(command).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
…utable token Address CodeRabbit review feedback: - Quote stripping now removes all quote chars instead of only outer pairs, so concatenated fragments like "r"m, r"m", 'r''m' resolve to the actual command name and get caught by the blocklist. - Glob/wildcard rejection moved from validateCommand (entire input) into extractBaseCommand (executable token only), so legitimate args with wildcards like `ls *.txt` are no longer blocked.
|
CodeAnt AI is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI Incremental review completed. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/command-manager.ts (1)
250-253:⚠️ Potential issue | 🟠 MajorGlob bypass via fallback path to unprotected
getBaseCommand.When
extractBaseCommandreturnsnulldue to glob characters, the command isn't added toallCommands, making it empty. This triggers the fallback togetBaseCommand, which lacks both quote stripping and glob rejection.Bypass example:
- Input:
/usr/bin/su*oorsu*oextractBaseCommandreturnsnull(glob detected)allCommandsis empty → fallback triggeredgetBaseCommandreturns/usr/bin/su*o(no protection)- Doesn't match
sudoin blocklist → command allowedConsider rejecting commands when
extractBaseCommandreturnsnulldue to glob patterns, or apply the same protections in the fallback path.🔒 Proposed fix: fail-closed when glob causes extraction failure
// If there are no commands extracted, fall back to base command if (allCommands.length === 0) { const baseCommand = this.getBaseCommand(command); + // Reject if the base command contains glob/wildcard characters + if (/[*?\[\]]/.test(baseCommand)) { + return false; + } return !blockedCommands.includes(baseCommand); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/command-manager.ts` around lines 250 - 253, The current fallback path uses getBaseCommand when allCommands is empty, allowing commands with globs to bypass checks; update the logic so that if extractBaseCommand returns null for any input (indicating a glob/invalid extraction) you fail-closed — i.e., treat the command as blocked instead of falling back — or alternatively make the fallback call reuse the same protections as extractBaseCommand (strip quotes and detect/reject glob patterns) before consulting blockedCommands; locate references to allCommands, extractBaseCommand, and getBaseCommand in the command filtering code and enforce the fail-closed behavior or identical sanitization on the fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/command-manager.ts`:
- Around line 250-253: The current fallback path uses getBaseCommand when
allCommands is empty, allowing commands with globs to bypass checks; update the
logic so that if extractBaseCommand returns null for any input (indicating a
glob/invalid extraction) you fail-closed — i.e., treat the command as blocked
instead of falling back — or alternatively make the fallback call reuse the same
protections as extractBaseCommand (strip quotes and detect/reject glob patterns)
before consulting blockedCommands; locate references to allCommands,
extractBaseCommand, and getBaseCommand in the command filtering code and enforce
the fail-closed behavior or identical sanitization on the fallback.
|
Friendly bump -- let me know if anything needs changing. |
When extractBaseCommand returns null (e.g. due to glob in the command name), validateCommand falls back to getBaseCommand which had no glob protection. This allowed e.g. /usr/bin/su*o to bypass the blocklist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
CodeAnt AI is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI Incremental review completed. |
|
Added a glob/wildcard check in the |
|
Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit. |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/command-manager.ts (1)
250-258:⚠️ Potential issue | 🔴 CriticalFallback-only glob check leaves the common path unprotected.
This guard only fires when
extractCommandsreturned an empty array, i.e. the input is a single unparseable token. As soon asextractCommandsproduces at least one benign base command, control skips this branch entirely and any glob-containing sibling segment (already silently dropped byextractBaseCommandreturningnull) escapes validation. See the detailed repro and fix in the comment on lines 225–229 — resolving that comment fixes this gap as well.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/command-manager.ts` around lines 250 - 258, The current glob-wildcard guard in the fallback branch only runs when extractCommands yields no parses; fix by validating every extracted command instead of only the empty case: in the code paths using extractCommands (and inside getBaseCommand/extractBaseCommand flows), iterate allCommands (and treat any null/undefined base returned by extractBaseCommand as a rejection) and run the glob regex /[*?\[\]]/ against each baseCommand, returning false if any base contains a glob or any base is missing; ensure blockedCommands check is applied per-base as well so no glob-containing sibling segments slip through.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/command-manager.ts`:
- Line 227: Update the command rejection regex to include brace-expansion
characters so brace patterns like /usr/bin/s{udo,x} are caught; find the checks
that test firstToken with the pattern /[*?\[\]]/ (around where firstToken is
examined) and replace both occurrences (the one at the firstToken check and the
later similar check near line 254) with a character class that also includes {
and } (i.e., add curly braces to the class) so brace expansion is rejected
alongside *, ?, and bracket patterns.
---
Duplicate comments:
In `@src/command-manager.ts`:
- Around line 250-258: The current glob-wildcard guard in the fallback branch
only runs when extractCommands yields no parses; fix by validating every
extracted command instead of only the empty case: in the code paths using
extractCommands (and inside getBaseCommand/extractBaseCommand flows), iterate
allCommands (and treat any null/undefined base returned by extractBaseCommand as
a rejection) and run the glob regex /[*?\[\]]/ against each baseCommand,
returning false if any base contains a glob or any base is missing; ensure
blockedCommands check is applied per-base as well so no glob-containing sibling
segments slip through.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
…to blocklist
- When extractBaseCommand returns null (glob/invalid), push sentinel
'__INVALID_COMMAND__' instead of silently dropping the segment. validateCommand
now rejects the entire input if any segment is invalid, preventing e.g.
'ls && su*o' from bypassing the blocklist.
- Extend the glob regex to include brace-expansion chars ({}) so
'/usr/bin/s{udo,x}' is also rejected.
|
Addressed the latest coderabbitai feedback:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/command-manager.ts (1)
213-221:$()-token branch inextractBaseCommandskips quote-stripping and glob rejection.This branch is mostly unreachable in practice because
extractCommandsintercepts$()at the character level (lines 60–81), but it is reachable via the error fallback at line 172 (extractBaseCommand(commandString)whenextractCommandsthrows). IffirstTokenis something like$(rm*)or$("r"m), this path returnsrm*/"r"mwithout applying the quote-strip or glob check added below, so the sentinel/rejection paths never fire.For defense-in-depth consistency, route the inner token through the same normalization used at lines 225–231 (strip quotes, reject glob/brace) before returning.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/command-manager.ts` around lines 213 - 221, The $()-token branch in extractBaseCommand currently returns the raw inner token and bypasses the quote-stripping and glob/brace rejection used later; update the branch to normalize the inner token the same way as the main path: after computing inner and innerTokens, set the candidate to path.basename(innerTokens[0]).toLowerCase(), then strip surrounding single/double quotes and run the same glob/brace rejection logic used later (the same checks applied to firstToken) before returning, so extractBaseCommand and the fallback from extractCommands behave consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/command-manager.ts`:
- Around line 139-143: When extractBaseCommand(currentCmd.trim()) returns null,
only push the fail-closed sentinel '__INVALID_COMMAND__' if the original segment
actually contains glob/brace characters; otherwise keep the previous silent-drop
behavior for benign nulls (env-var only segments, bare $VARIABLE/paren tokens,
empty $() or thrown exceptions). Concretely: compute a quick check for
glob/brace characters in the segment (e.g. presence of '*', '?', '[', ']', '{',
'}') and change the block that currently does commands.push(baseCommand !== null
? baseCommand : '__INVALID_COMMAND__') to push the sentinel only when
baseCommand === null && segmentContainsGlob; do the same modification in the
other place that handles the final segment where baseCommand is treated
similarly.
---
Nitpick comments:
In `@src/command-manager.ts`:
- Around line 213-221: The $()-token branch in extractBaseCommand currently
returns the raw inner token and bypasses the quote-stripping and glob/brace
rejection used later; update the branch to normalize the inner token the same
way as the main path: after computing inner and innerTokens, set the candidate
to path.basename(innerTokens[0]).toLowerCase(), then strip surrounding
single/double quotes and run the same glob/brace rejection logic used later (the
same checks applied to firstToken) before returning, so extractBaseCommand and
the fallback from extractCommands behave consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| if (currentCmd.trim()) { | ||
| const baseCommand = this.extractBaseCommand(currentCmd.trim()); | ||
| if (baseCommand) commands.push(baseCommand); | ||
| // null means glob/invalid — push sentinel so validateCommand fails closed | ||
| commands.push(baseCommand !== null ? baseCommand : '__INVALID_COMMAND__'); | ||
| } |
There was a problem hiding this comment.
Fail-closed sentinel is pushed for all extractBaseCommand nulls, not only glob rejection — regresses legitimate multi-segment inputs.
extractBaseCommand returns null in several non-malicious cases beyond glob detection:
- Segment is only env-var assignments, e.g.
FOO=bar→ line 181 strips to empty → line 184 returnsnull. - Segment is only dollar-variable /
(tokens, e.g.$MYVAR→ loop at lines 191–206 never setsfirstToken→ line 210 returnsnull. $()with empty inner (line 220).- Any thrown exception (line 238).
Previously these were silently dropped by the if (baseCommand) guard. With the new sentinel push, benign inputs such as FOO=bar; ls or $MYVAR && ls are now blocked outright via the check at line 264, which was not an intended goal of #421/#422 and will surface as Error: Command not allowed: ... in the caller (src/tools/improved-process-tools.ts lines 119–125).
Disambiguate: only emit the sentinel when the segment actually contains glob/brace characters, and keep the silent-drop behavior for the other null causes.
🛡️ Proposed fix — gate sentinel on glob presence in the segment
if (commandString.startsWith(separator, i)) {
// We found a separator - extract the command before it
if (currentCmd.trim()) {
- const baseCommand = this.extractBaseCommand(currentCmd.trim());
- // null means glob/invalid — push sentinel so validateCommand fails closed
- commands.push(baseCommand !== null ? baseCommand : '__INVALID_COMMAND__');
+ const trimmed = currentCmd.trim();
+ const baseCommand = this.extractBaseCommand(trimmed);
+ if (baseCommand !== null) {
+ commands.push(baseCommand);
+ } else if (/[*?\[\]{}]/.test(trimmed)) {
+ // Segment contained a glob/brace token we could not
+ // safely resolve — fail closed.
+ commands.push('__INVALID_COMMAND__');
+ }
+ // else: env-vars-only / variable-only / empty — drop silently.
}Apply the same treatment to the final-segment block at lines 159–163.
Also applies to: 159-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/command-manager.ts` around lines 139 - 143, When
extractBaseCommand(currentCmd.trim()) returns null, only push the fail-closed
sentinel '__INVALID_COMMAND__' if the original segment actually contains
glob/brace characters; otherwise keep the previous silent-drop behavior for
benign nulls (env-var only segments, bare $VARIABLE/paren tokens, empty $() or
thrown exceptions). Concretely: compute a quick check for glob/brace characters
in the segment (e.g. presence of '*', '?', '[', ']', '{', '}') and change the
block that currently does commands.push(baseCommand !== null ? baseCommand :
'__INVALID_COMMAND__') to push the sentinel only when baseCommand === null &&
segmentContainsGlob; do the same modification in the other place that handles
the final segment where baseCommand is treated similarly.
|
Friendly bump -- let me know if anything needs changing. |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR hardens command validation by splitting chained commands on newline separators, normalizing quoted executable names, rejecting glob and brace patterns, and failing closed when any segment is unsafe before checking against the blocked commands list. sequenceDiagram
participant Caller
participant CommandManager
participant Config
Caller->>CommandManager: validateCommand(command string)
CommandManager->>Config: load blockedCommands
Config-->>CommandManager: blockedCommands list
CommandManager->>CommandManager: split command by separators including newlines
CommandManager->>CommandManager: extract base commands (strip quotes, reject glob or brace patterns, mark invalid segments)
alt any segment invalid or in blockedCommands
CommandManager-->>Caller: validation failed (do not execute)
else all segments safe
CommandManager-->>Caller: validation passed (may execute)
end
Generated by CodeAnt AI |
|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
Closing due to inactivity. Happy to reopen if there's interest. |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR strengthens command validation before starting external processes by splitting chained commands on newlines and rejecting commands whose executable name is hidden by quotes or wildcard patterns that bypass the blocklist. sequenceDiagram
participant Client
participant ProcessTool
participant CommandManager
participant ConfigManager
participant Shell
Client->>ProcessTool: Request start process with command
ProcessTool->>CommandManager: Validate command string
CommandManager->>CommandManager: Extract base commands handling newlines quotes and glob patterns
CommandManager->>ConfigManager: Load blocked commands
ConfigManager-->>CommandManager: Return blocked command list
alt Any segment invalid or blocked
CommandManager-->>ProcessTool: Reject command as not allowed
ProcessTool-->>Client: Return command not allowed error
else All segments valid and not blocked
CommandManager-->>ProcessTool: Command is allowed
ProcessTool->>Shell: Execute command
Shell-->>Client: Return command output
end
Generated by CodeAnt AI |
| const blockedCommands = config.blockedCommands || []; | ||
|
|
||
| // Extract all commands from the command string |
There was a problem hiding this comment.
🔴 Architect Review — CRITICAL
Blocked-command validation can still be bypassed via shell variable expansion of the executable name. For a command like export X=sudo; $X whoami, extractCommands/extractBaseCommand skip the $X token and instead record export and whoami, so sudo is never compared against blockedCommands even though the shell executes sudo.
Suggestion: In extractBaseCommand/extractCommands, treat dollar-prefixed tokens used in command position as unsafe rather than skipping them: either reject commands whose effective executable is variable-based (e.g. $VAR, ${VAR}) or adopt a parsing/policy approach that only allows literal command names and fails closed when the invoked binary cannot be determined statically.
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.
**Path:** src/command-manager.ts
**Line:** 248:271
**Comment:**
*CRITICAL: Blocked-command validation can still be bypassed via shell variable expansion of the executable name. For a command like `export X=sudo; $X whoami`, extractCommands/extractBaseCommand skip the `$X` token and instead record `export` and `whoami`, so `sudo` is never compared against `blockedCommands` even though the shell executes `sudo`.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const baseCommand = this.extractBaseCommand(currentCmd.trim()); | ||
| if (baseCommand) commands.push(baseCommand); | ||
| // null means glob/invalid — push sentinel so validateCommand fails closed | ||
| commands.push(baseCommand !== null ? baseCommand : '__INVALID_COMMAND__'); |
There was a problem hiding this comment.
Suggestion: Treating every null from command extraction as __INVALID_COMMAND__ causes legitimate command chains to be rejected, because extractBaseCommand returns null for non-malicious segments like env-only assignments (for example FOO=bar && echo ok). Only map to an invalid sentinel when the token is explicitly unsafe (glob/brace pattern), and skip segments that simply do not represent an executable command. [logic error]
Severity Level: Major ⚠️
- ⚠️ start_process rejects commands with leading env-only assignments.
- ⚠️ Benign multi-part shell commands fail with "Command not allowed".Steps of Reproduction ✅
1. Start the MCP server defined in `src/server.ts`, which registers the `start_process`
tool in the tools list (see `server.ts:795-868` where the `"start_process"` tool is
declared with `StartProcessArgsSchema`).
2. From an MCP client, invoke `CallToolRequest` with `name: "start_process"` so that
`server.ts:1170-1177` routes to `case "start_process":` at `server.ts:1337-1339`, calling
`handlers.handleStartProcess(args)` in `src/handlers/terminal-handlers.ts:22-25`.
3. In `handleStartProcess`, `StartProcessArgsSchema.parse(args)` (defined in
`src/tools/schemas.ts:22-27`) produces `{ command: "FOO=bar && echo ok", timeout_ms: 5000,
... }` and passes it to `startProcess(parsed)`
(`src/tools/improved-process-tools.ts:97-105`), which then calls
`commandManager.validateCommand(parsed.data.command)` at `improved-process-tools.ts:119`.
4. Inside `validateCommand` (`src/command-manager.ts:242-276`), `extractCommands(command)`
is called. For the first segment `"FOO=bar"`: `extractCommands`
(`command-manager.ts:11-167`) hits the `"&&"` separator and calls
`extractBaseCommand(currentCmd.trim())` at `command-manager.ts:140`. `extractBaseCommand`
removes env vars via `const withoutEnvVars = commandStr.replace(/\w+=\S+\s*/g,
'').trim();` at `command-manager.ts:181`, leaving an empty string and returning `null` at
`command-manager.ts:184`. Back in `extractCommands`, the null is converted to the sentinel
by `commands.push(baseCommand !== null ? baseCommand : '__INVALID_COMMAND__');` at
`command-manager.ts:142`, so the overall `allCommands` array becomes
`['__INVALID_COMMAND__', 'echo']`. `validateCommand` then executes `if
(allCommands.includes('__INVALID_COMMAND__')) { return false; }` at
`command-manager.ts:262-265`, causing `startProcess` to return `{ isError: true, content:
"Error: Command not allowed: FOO=bar && echo ok" }` (`improved-process-tools.ts:119-125`),
even though the only executable command is the benign `echo`.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/command-manager.ts
**Line:** 142:142
**Comment:**
*Logic Error: Treating every `null` from command extraction as `__INVALID_COMMAND__` causes legitimate command chains to be rejected, because `extractBaseCommand` returns `null` for non-malicious segments like env-only assignments (for example `FOO=bar && echo ok`). Only map to an invalid sentinel when the token is explicitly unsafe (glob/brace pattern), and skip segments that simply do not represent an executable command.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR strengthens command validation by splitting chained commands on newlines, normalizing quoted executables, and rejecting commands that use glob or brace patterns to bypass the blocklist. sequenceDiagram
participant Caller
participant CommandManager
participant ConfigManager
Caller->>CommandManager: validateCommand(command string)
CommandManager->>CommandManager: extractCommands with separators and newlines
alt One or more commands extracted
CommandManager->>CommandManager: normalize executable, strip quotes, reject glob patterns
CommandManager->>ConfigManager: load blockedCommands
CommandManager-->>Caller: Reject if blocked or invalid segment
else No commands extracted
CommandManager->>CommandManager: getBaseCommand and check glob chars
CommandManager->>ConfigManager: load blockedCommands
CommandManager-->>Caller: Allow or reject based on blocklist
end
Generated by CodeAnt AI |
|
|
||
| // Strip all quote characters so that concatenated fragments like | ||
| // "r"m, r"m", 'r''m' are normalized to the actual command name (#421) | ||
| firstToken = firstToken.replace(/["']/g, ''); |
There was a problem hiding this comment.
Suggestion: Stripping only quote characters is not enough to normalize shell command tokens: backslash escapes are still left intact. A blocked command can still be bypassed with input like r\m -rf /, because validation compares r\m against the blocklist while the shell resolves it to rm at execution time. Normalize shell escapes (or reject escaped command-name tokens) before blocklist comparison. [security]
Severity Level: Critical 🚨
- ❌ Blocked commands executable via backslash-escaped names.
- ❌ start_process tool can't reliably enforce command blocklist.
- ⚠️ Potential privilege escalation via sudo, su commands.Steps of Reproduction ✅
1. Start the MCP server defined in `src/server.ts` so the `start_process` tool is
registered (terminal tools section around lines 797–816 describes `start_process` as the
primary terminal tool).
2. Note that the default config in `src/config-manager.ts:109-155` includes `"sudo"` in
`blockedCommands`, so `sudo` should always be rejected by validation.
3. From a MCP client, call the `start_process` tool (handled by `handleStartProcess` in
`src/handlers/terminal-handlers.ts:13-16`, which calls `startProcess` in
`src/tools/improved-process-tools.ts:97-127`) with a command like `s\udo id`.
4. Inside `startProcess`, the call `commandManager.validateCommand(parsed.data.command)`
at `src/tools/improved-process-tools.ts:119-125` invokes `validateCommand` in
`src/command-manager.ts:242-276`, which calls `extractCommands(command)` at line 248;
`extractCommands` builds the token `s\udo` into `currentCmd` (lines 29-45) and passes it
to `extractBaseCommand` at `src/command-manager.ts:177-235`, where `firstToken` remains
`s\udo` except for quote-stripping via `firstToken = firstToken.replace(/["']/g, '');` at
line 225; since there is no backslash normalization, `validateCommand` compares `"s\\udo"`
against `blockedCommands` (lines 268-273), does not match `"sudo"`, returns `true`, and
`startProcess` executes the original `s\udo id` via `terminalManager.executeCommand` at
`src/tools/improved-process-tools.ts:170-175`, allowing the blocked `sudo` command to run
because the shell interprets `s\udo` as `sudo`.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/command-manager.ts
**Line:** 225:225
**Comment:**
*Security: Stripping only quote characters is not enough to normalize shell command tokens: backslash escapes are still left intact. A blocked command can still be bypassed with input like `r\m -rf /`, because validation compares `r\m` against the blocklist while the shell resolves it to `rm` at execution time. Normalize shell escapes (or reject escaped command-name tokens) before blocklist comparison.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if (allCommands.includes('__INVALID_COMMAND__')) { | ||
| return false; |
There was a problem hiding this comment.
Suggestion: The new fail-closed sentinel check rejects commands whenever extractBaseCommand() returns null, but null is also used for non-malicious segments (for example, standalone environment assignments like FOO=1 && echo ok). This causes valid command chains to be denied. Differentiate "invalid/dangerous token" from "no executable command in this segment" instead of treating all null results as fatal. [logic error]
Severity Level: Major ⚠️
- ❌ start_process rejects chains with env-only command segments.
- ⚠️ Users can't run some valid shell workflows.
- ⚠️ Overzealous validation reduces terminal tool usability.Steps of Reproduction ✅
1. Start the MCP server from `src/server.ts`, which registers the `start_process` tool
(terminal tools block around `src/server.ts:797-816`) and uses `handleStartProcess` in
`src/handlers/terminal-handlers.ts:13-16` to route requests.
2. From a MCP client, invoke the `start_process` tool with a command string like `FOO=1 &&
echo ok`, so `handleStartProcess` calls `startProcess(parsed)` in
`src/tools/improved-process-tools.ts:97-127`.
3. Inside `startProcess`, the call `const isAllowed = await
commandManager.validateCommand(parsed.data.command);` at
`src/tools/improved-process-tools.ts:119-125` routes to `validateCommand` in
`src/command-manager.ts:242-276`, which first builds `allCommands` by calling
`this.extractCommands(command)` at line 248; `extractCommands` splits on `&&` using the
`separators` array (lines 16-18, 134-151), producing segments `"FOO=1"` and `"echo ok"`
that are each passed to `extractBaseCommand`.
4. For the `"FOO=1"` segment, `extractBaseCommand` in `src/command-manager.ts:177-235`
removes env assignments via `const withoutEnvVars = commandStr.replace(/\w+=\S+\s*/g,
'').trim();` at lines 181-184, leaving an empty string and returning `null` (lines
183-184); `extractCommands` then pushes the sentinel `"__INVALID_COMMAND__"` for this
segment at line 162, so `allCommands` becomes `["__INVALID_COMMAND__","echo"]`, and back
in `validateCommand` the fail-closed check `if
(allCommands.includes('__INVALID_COMMAND__')) { return false; }` at lines 262-265 rejects
the entire command even though the second segment `"echo ok"` yields an allowed base
command `"echo"`, causing `startProcess` in `src/tools/improved-process-tools.ts:119-125`
to return `Error: Command not allowed: FOO=1 && echo ok` and blocking a valid command
chain that just uses an env-only segment.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/command-manager.ts
**Line:** 264:265
**Comment:**
*Logic Error: The new fail-closed sentinel check rejects commands whenever `extractBaseCommand()` returns `null`, but `null` is also used for non-malicious segments (for example, standalone environment assignments like `FOO=1 && echo ok`). This causes valid command chains to be denied. Differentiate "invalid/dangerous token" from "no executable command in this segment" instead of treating all `null` results as fatal.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR strengthens command validation by correctly splitting on newlines, normalizing quoted command names, and rejecting commands that contain unsafe glob or brace patterns before checking them against the blocklist. sequenceDiagram
participant Caller
participant CommandManager
participant ConfigManager
Caller->>CommandManager: validate command string
CommandManager->>ConfigManager: load blocked commands
ConfigManager-->>CommandManager: return blockedCommands
CommandManager->>CommandManager: split by separators and newlines, normalize base commands and flag glob or invalid tokens
alt no commands extracted
CommandManager->>CommandManager: derive base command and check for glob characters
end
alt any invalid or blocked command
CommandManager-->>Caller: reject command
else all commands allowed
CommandManager-->>Caller: accept command
end
Generated by CodeAnt AI |
| // Strip all quote characters so that concatenated fragments like | ||
| // "r"m, r"m", 'r''m' are normalized to the actual command name (#421) | ||
| firstToken = firstToken.replace(/["']/g, ''); |
There was a problem hiding this comment.
Suggestion: Quote normalization is incomplete because it only removes quote characters and does not normalize shell escape characters. On POSIX shells, an attacker can still obfuscate a blocked command with backslash escaping (for example r\m), which the shell resolves to rm at execution time while the validator compares the unnormalized token and lets it pass. Normalize shell escapes before blocklist comparison (or parse using a shell-aware tokenizer). [security]
Severity Level: Critical 🚨
- ❌ Blocked commands bypassed via backslash-escaped names.
- ❌ Malicious shell commands can run on host system.
- ⚠️ BlockedCommands security guarantees undermined for escaped inputs.Steps of Reproduction ✅
1. Invoke the MCP `start_process` tool via the server entrypoint `handleStartProcess` in
`src/handlers/terminal-handlers.ts:5-7`, passing args matching `StartProcessArgsSchema`
(`src/tools/schemas.ts:11-16`) with `command: "r\\m -rf /tmp/testdir"` and `timeout_ms`
set to any valid number.
2. `handleStartProcess` forwards the parsed args to `startProcess` in
`src/tools/improved-process-tools.ts:18-27`, which records telemetry and then calls
`commandManager.validateCommand(parsed.data.command)` at
`src/tools/improved-process-tools.ts:40`.
3. Inside `validateCommand` (`src/command-manager.ts:242-260`), `extractCommands`
(`src/command-manager.ts:11-21`) processes the string `r\\m -rf /tmp/testdir`. It never
hits a separator, so `currentCmd` becomes `r\\m -rf /tmp/testdir`. At the end of the loop
(`src/command-manager.ts:158-163`), `extractBaseCommand(currentCmd.trim())` is called,
which returns the first token `r\\m` (after env-var stripping and quote stripping at
`src/command-manager.ts:180-225`) and does not treat the backslash as special.
4. `extractCommands` returns `["r\\m"]`, so `validateCommand` sees `allCommands =
["r\\m"]` (`src/command-manager.ts:248-260`), does not trigger the `__INVALID_COMMAND__`
sentinel check (`src/command-manager.ts:262-265`), and compares `"r\\m"` against
`blockedCommands` from config (e.g. a user-configured `"rm"` entry in `blockedCommands`
loaded by `configManager.getConfig()` at `src/config-manager.ts:191-193`). Since `"r\\m"
!== "rm"`, validation passes and `terminalManager.executeCommand`
(`src/terminal-manager.ts:10-69`) runs the command via `/bin/sh -c "r\\m -rf
/tmp/testdir"` or similar; in POSIX shells (see `getShellSpawnArgs` at
`src/terminal-manager.ts:13-21,43-47`), `r\\m` is interpreted as `rm`, so the blocked `rm`
executes despite being on the blocklist.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/command-manager.ts
**Line:** 223:225
**Comment:**
*Security: Quote normalization is incomplete because it only removes quote characters and does not normalize shell escape characters. On POSIX shells, an attacker can still obfuscate a blocked command with backslash escaping (for example `r\m`), which the shell resolves to `rm` at execution time while the validator compares the unnormalized token and lets it pass. Normalize shell escapes before blocklist comparison (or parse using a shell-aware tokenizer).
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| // Fail closed: if any segment failed extraction (e.g. contained a glob), | ||
| // reject the entire command rather than silently ignoring the bad segment. | ||
| if (allCommands.includes('__INVALID_COMMAND__')) { | ||
| return false; |
There was a problem hiding this comment.
Suggestion: This fail-closed sentinel logic treats every null extraction result as malicious, but extractBaseCommand() also returns null for benign inputs such as environment-variable-only commands (for example FOO=bar). That causes valid commands to be rejected unexpectedly. Return a typed extraction result that distinguishes "no executable token" from "invalid/malicious token", and only hard-fail for the malicious case. [logic error]
Severity Level: Major ⚠️
- ⚠️ Benign env-prefixed commands rejected as disallowed.
- ⚠️ Multi-segment commands with env setup cannot run.
- ⚠️ start_process usability reduced for shell-style workflows.Steps of Reproduction ✅
1. Call the MCP `start_process` tool through `handleStartProcess` at
`src/handlers/terminal-handlers.ts:5-7` with args matching `StartProcessArgsSchema`
(`src/tools/schemas.ts:11-16`), using a command that starts with an environment-only
segment followed by a benign command, e.g. `command: "FOO=bar; ls"` and any valid
`timeout_ms`.
2. `handleStartProcess` passes the parsed args to `startProcess`
(`src/tools/improved-process-tools.ts:18-27`), which then calls
`commandManager.validateCommand(parsed.data.command)` at
`src/tools/improved-process-tools.ts:40` before executing anything.
3. `validateCommand` (`src/command-manager.ts:242-260`) calls `extractCommands(command)`
(`src/command-manager.ts:11-21`). Inside `extractCommands`, the loop accumulates
`currentCmd = "FOO=bar"` until it hits the `;` separator (separators defined at
`src/command-manager.ts:16-18`). At `src/command-manager.ts:138-143`, it calls
`extractBaseCommand(currentCmd.trim())` with `"FOO=bar"`.
4. In `extractBaseCommand` (`src/command-manager.ts:177-185`), the env-var stripping regex
`commandStr.replace(/\w+=\S+\s*/g, '')` removes `"FOO=bar"`, leaving an empty string; the
function returns `null` at `src/command-manager.ts:183-184`. Back in `extractCommands`,
this `null` result is treated as an invalid/malicious segment and replaced with the
sentinel `'__INVALID_COMMAND__'` (`src/command-manager.ts:140-142`). The second segment
(`" ls"`) yields a valid base command `"ls"`, so `allCommands` becomes
`['__INVALID_COMMAND__', 'ls']`. In `validateCommand`, the presence of
`'__INVALID_COMMAND__'` triggers the fail-closed path at `src/command-manager.ts:262-265`,
returning `false` and causing `startProcess` (`src/tools/improved-process-tools.ts:40-45`)
to reject the entire user command `"FOO=bar; ls"` as disallowed, even though it contains
only benign operations.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/command-manager.ts
**Line:** 262:265
**Comment:**
*Logic Error: This fail-closed sentinel logic treats every `null` extraction result as malicious, but `extractBaseCommand()` also returns `null` for benign inputs such as environment-variable-only commands (for example `FOO=bar`). That causes valid commands to be rejected unexpectedly. Return a typed extraction result that distinguishes "no executable token" from "invalid/malicious token", and only hard-fail for the malicious case.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR hardens command validation so that chained commands using newlines, quoted executables, or wildcard-based paths cannot bypass the blocked command list, and invalid segments cause the entire request to be rejected. sequenceDiagram
participant Caller
participant CommandManager
participant Config
participant Shell
Caller->>CommandManager: Request to run command string
CommandManager->>CommandManager: Split string by separators including newlines
CommandManager->>CommandManager: Extract base commands, strip quotes, reject glob and brace patterns
CommandManager->>Config: Load blocked command list
Config-->>CommandManager: Return blocked commands
alt Command is blocked or any segment is invalid
CommandManager-->>Caller: Reject command
else Command is allowed
CommandManager->>Shell: Execute validated command
Shell-->>Caller: Return command result
end
Generated by CodeAnt AI |
|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
User description
Summary
Fixes two critical command injection vulnerabilities in
src/command-manager.tsthat allow complete bypass of theblockedCommandssecurity feature:Newline bypass ([Security] Command Injection via Blocklist Bypass using Newline Character #422):
extractCommands()did not treat newline characters as command separators. An attacker could injectecho benign\nrm -rf /and validation would only checkecho, while the shell would execute both commands. Fixed by adding\r\n,\n,\rto the separators array.Quoting and wildcard bypass ([Security] Command Injection via Blocklist Bypass using Quoting and Wildcards #421):
extractBaseCommand()did not strip quotes from tokens, so"rm"would not match the blocked commandrm. Wildcard paths like/usr/bin/su*owould also evade literal blocklist matching but expand at shell execution time. Fixed by stripping surrounding quotes before comparison and rejecting any command containing glob characters (*,?,[,]).Changes
All changes are in
src/command-manager.ts:\r\n,\n,\rto theseparatorsarray inextractCommands()extractBaseCommand()extractBaseCommand()andvalidateCommand()Test plan
echo benign\nrm fileis correctly split into two commands andrmis blocked"rm" fileis correctly identified asrmand blocked/usr/bin/su*ois rejected outright due to glob charactersnpm test)Closes #421
Closes #422
Summary by CodeRabbit
CodeAnt-AI Description
Block command-bypass tricks in command validation
What Changed
Impact
✅ Fewer command injection bypasses✅ Safer blocked-command enforcement✅ Clearer rejection of unsafe command patterns🔄 Retrigger CodeAnt AI Review
Details
💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.