Skip to content

Fix command injection via newline and quoting/wildcard blocklist bypass#429

Closed
Christian-Sidak wants to merge 4 commits intowonderwhy-er:mainfrom
Christian-Sidak:fix/command-injection-blocklist-bypass
Closed

Fix command injection via newline and quoting/wildcard blocklist bypass#429
Christian-Sidak wants to merge 4 commits intowonderwhy-er:mainfrom
Christian-Sidak:fix/command-injection-blocklist-bypass

Conversation

@Christian-Sidak
Copy link
Copy Markdown

@Christian-Sidak Christian-Sidak commented Apr 10, 2026

User description

Summary

Fixes two critical command injection vulnerabilities in src/command-manager.ts that allow complete bypass of the blockedCommands security 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 inject echo benign\nrm -rf / and validation would only check echo, while the shell would execute both commands. Fixed by adding \r\n, \n, \r to 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 command rm. Wildcard paths like /usr/bin/su*o would 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:

  • Add \r\n, \n, \r to the separators array in extractCommands()
  • Strip surrounding single/double quotes from command tokens in extractBaseCommand()
  • Reject commands containing glob/wildcard characters in both extractBaseCommand() and validateCommand()

Test plan

  • Verify echo benign\nrm file is correctly split into two commands and rm is blocked
  • Verify "rm" file is correctly identified as rm and blocked
  • Verify /usr/bin/su*o is rejected outright due to glob characters
  • Verify normal commands without quotes, globs, or newlines continue to work
  • Verify existing test suite passes (npm test)

Closes #421
Closes #422

Summary by CodeRabbit

  • Bug Fixes
    • Commands containing wildcard or brace characters are now rejected to prevent unsafe patterns.
    • Line breaks are recognized and treated as separators so each line is validated independently.
    • Command tokens are normalized (quotes removed) for consistent validation.
    • Inputs fail-closed: any invalid segment now causes the whole input to be rejected.

CodeAnt-AI Description

Block command-bypass tricks in command validation

What Changed

  • Commands split across new lines are now checked separately, so a hidden second command is not allowed through
  • Quoted command names and mixed quote fragments are now recognized as the real command, so blocked commands cannot slip past by adding quotes
  • Commands with wildcard or brace patterns in the command name are now rejected instead of being treated as safe

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:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

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:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

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.

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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 10, 2026

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 ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

extractCommands 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

Cohort / File(s) Summary
Command Parsing & Validation
src/command-manager.ts
extractCommands() splits on \r\n, \n, \r; extractBaseCommand() removes single/double quotes from the first token and returns null for tokens containing glob/wildcard (* ? [ ]) or brace ({ }) chars (pushes '__INVALID_COMMAND__' sentinel). validateCommand() fail-closes when extracted list is empty and fallback contains globs or when any segment is '__INVALID_COMMAND__'.

Sequence Diagram(s)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

size:L

Suggested reviewers

  • es-dc

Poem

🐰 I hopped through strings and split each line,
I peeled off quotes so tokens shine,
I sniffed for stars and braces too,
Sent invalid hops back to you,
A safer warren — carrot-cheer, hooray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing command injection vulnerabilities via newline separator bypass and quoting/wildcard blocklist bypass.
Linked Issues check ✅ Passed The PR addresses all core requirements from #421 and #422: adding newline separators, stripping quotes, rejecting glob/brace-expansion characters, and implementing fail-closed validation with sentinel propagation.
Out of Scope Changes check ✅ Passed All changes in src/command-manager.ts are directly scoped to fixing the two command-injection vulnerabilities described in the linked issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:S This PR changes 10-29 lines, ignoring generated files label Apr 10, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 10, 2026

CodeAnt AI finished reviewing your PR.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b958ab6e-f205-404e-8dcf-fc5139545bdc

📥 Commits

Reviewing files that changed from the base of the PR and between eb139ac and b3259de.

📒 Files selected for processing (1)
  • src/command-manager.ts

Comment thread src/command-manager.ts Outdated
Comment thread src/command-manager.ts Outdated
…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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 11, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels Apr 11, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 11, 2026

CodeAnt AI Incremental review completed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Glob bypass via fallback path to unprotected getBaseCommand.

When extractBaseCommand returns null due to glob characters, the command isn't added to allCommands, making it empty. This triggers the fallback to getBaseCommand, which lacks both quote stripping and glob rejection.

Bypass example:

  1. Input: /usr/bin/su*o or su*o
  2. extractBaseCommand returns null (glob detected)
  3. allCommands is empty → fallback triggered
  4. getBaseCommand returns /usr/bin/su*o (no protection)
  5. Doesn't match sudo in blocklist → command allowed

Consider rejecting commands when extractBaseCommand returns null due 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d8b2aa4-f3ca-4a93-a716-a4612199cafe

📥 Commits

Reviewing files that changed from the base of the PR and between b3259de and 1d994f6.

📒 Files selected for processing (1)
  • src/command-manager.ts

@Christian-Sidak
Copy link
Copy Markdown
Author

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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 24, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels Apr 24, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 24, 2026

CodeAnt AI Incremental review completed.

@Christian-Sidak
Copy link
Copy Markdown
Author

Added a glob/wildcard check in the getBaseCommand fallback path inside validateCommand. This closes the bypass @coderabbitai flagged where extractBaseCommand returning null (due to a glob in the command name) would fall through to the unprotected getBaseCommand, allowing e.g. /usr/bin/su*o to skip the blocklist.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/command-manager.ts (1)

250-258: ⚠️ Potential issue | 🔴 Critical

Fallback-only glob check leaves the common path unprotected.

This guard only fires when extractCommands returned an empty array, i.e. the input is a single unparseable token. As soon as extractCommands produces at least one benign base command, control skips this branch entirely and any glob-containing sibling segment (already silently dropped by extractBaseCommand returning null) 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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3acacbad-9765-48de-b0d5-62c369455ca2

📥 Commits

Reviewing files that changed from the base of the PR and between 1d994f6 and c5d48ea.

📒 Files selected for processing (1)
  • src/command-manager.ts

Comment thread src/command-manager.ts Outdated
Comment thread src/command-manager.ts Outdated
…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.
@Christian-Sidak
Copy link
Copy Markdown
Author

Addressed the latest coderabbitai feedback:

  1. Fail-closed on glob in any segmentextractCommands now pushes a sentinel ('__INVALID_COMMAND__') when extractBaseCommand returns null (i.e., glob detected), rather than silently dropping the segment. validateCommand rejects immediately if the sentinel is present, so ls && su*o no longer bypasses the blocklist via the benign ls.

  2. Brace expansion added to glob regex — Updated /[*?\[\]]/ to /[*?\[\]{}]/ in both extractBaseCommand and the fallback glob check in validateCommand, so /usr/bin/s{udo,x} is caught alongside su*o patterns.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/command-manager.ts (1)

213-221: $()-token branch in extractBaseCommand skips quote-stripping and glob rejection.

This branch is mostly unreachable in practice because extractCommands intercepts $() at the character level (lines 60–81), but it is reachable via the error fallback at line 172 (extractBaseCommand(commandString) when extractCommands throws). If firstToken is something like $(rm*) or $("r"m), this path returns rm* / "r"m without 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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef91146f-2751-442e-bb68-5faabe042128

📥 Commits

Reviewing files that changed from the base of the PR and between c5d48ea and e7ceecb.

📒 Files selected for processing (1)
  • src/command-manager.ts

Comment thread src/command-manager.ts
Comment on lines 139 to 143
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__');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 returns null.
  • Segment is only dollar-variable / ( tokens, e.g. $MYVAR → loop at lines 191–206 never sets firstToken → line 210 returns null.
  • $() 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.

@Christian-Sidak
Copy link
Copy Markdown
Author

Friendly bump -- let me know if anything needs changing.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 1, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels May 1, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 1, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 1, 2026

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 ·
Reddit ·
LinkedIn

@Christian-Sidak
Copy link
Copy Markdown
Author

Closing due to inactivity. Happy to reopen if there's interest.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 3, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels May 3, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 3, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

Comment thread src/command-manager.ts
const blockedCommands = config.blockedCommands || [];

// Extract all commands from the command string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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

Comment thread src/command-manager.ts
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__');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 3, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels May 5, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

Comment thread src/command-manager.ts

// 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, '');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
👍 | 👎

Comment thread src/command-manager.ts
Comment on lines +264 to +265
if (allCommands.includes('__INVALID_COMMAND__')) {
return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels May 5, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

Comment thread src/command-manager.ts
Comment on lines +223 to +225
// 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, '');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
👍 | 👎

Comment thread src/command-manager.ts
Comment on lines +262 to +265
// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 5, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 7, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:S This PR changes 10-29 lines, ignoring generated files and removed size:S This PR changes 10-29 lines, ignoring generated files labels May 7, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 7, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented May 7, 2026

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 ·
Reddit ·
LinkedIn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files

Projects

None yet

1 participant