Skip to content

Potential fix for code scanning alert no. 11: Uncontrolled command line#7

Merged
involvex merged 1 commit intomainfrom
alert-autofix-11
Jan 31, 2026
Merged

Potential fix for code scanning alert no. 11: Uncontrolled command line#7
involvex merged 1 commit intomainfrom
alert-autofix-11

Conversation

@involvex
Copy link
Copy Markdown
Owner

@involvex involvex commented Jan 31, 2026

Potential fix for https://github.com/involvex/super-agent-cli/security/code-scanning/11

At a high level, we should stop passing a single, concatenated, untrusted string to child_process.exec, which invokes a shell. Instead, we should parse the command string into an executable and an array of arguments and execute it via spawn or execFile, which do not invoke a shell by default. For robust parsing that respects quotes and escapes, we should use a well‑known library like shell-quote.

Best, minimal‑impact fix:

  • In src/tools/bash.ts, replace execAsync(command, ...) with a safe wrapper that:
    • handles the special cd logic as today;
    • for all other commands:
      • uses shell-quote.parse to split the command string into tokens;
      • treats the first token as the program (cmd) and the rest as args;
      • executes via child_process.spawn with shell: false, capturing stdout and stderr and preserving timeout/maxBuffer semantics as closely as practical.
  • Import shell-quote and spawn in this file, but leave all other files and behavior unchanged.
  • Keep the public API of BashTool.execute and helper methods (listFiles, findFiles, grep) intact so callers like SuperAgent.executeBashCommand and use-input-handler.ts continue to work.

Concretely, in src/tools/bash.ts:

  1. Replace exec/execAsync usage with a new helper runCommandSafely(command, options) that:
    • uses shell-quote.parse to get tokens;
    • rejects empty commands;
    • spawns the child process and concatenates stdout/stderr strings;
    • enforces a timeout by killing the child if it runs too long;
    • enforces a maxBuffer by tracking accumulated output length and killing the child if exceeded.
  2. In execute, call runCommandSafely(command, { cwd: this.currentDirectory, timeout, maxBuffer: 1024*1024 }) instead of execAsync.
  3. Add the necessary imports at the top: spawn from child_process and shellQuote from shell-quote. Keep the existing exec/promisify import only if still needed; here we can safely remove exec/execAsync since they’re no longer used.

This eliminates shell interpretation of the user‑controlled string while preserving the ability to run arbitrary commands with arguments.


Suggested fixes powered by Copilot Autofix. Review carefully before merging.

Summary by Sourcery

Harden bash command execution by replacing shell-based exec usage with a safer spawn-based implementation that parses commands into argv tokens while preserving existing BashTool behavior.

Bug Fixes:

  • Prevent uncontrolled command execution by avoiding passing untrusted concatenated command strings to a shell.

Enhancements:

  • Introduce a runCommandSafely helper in BashTool that parses commands with shell-quote, enforces timeouts and output buffer limits, and captures stdout/stderr without invoking a shell.

Build:

  • Add shell-quote as a runtime dependency for safe command parsing.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jan 31, 2026

Reviewer's Guide

Replaces shell-based command execution in the Bash tool with a safer spawn-based implementation that parses the command string into argv tokens using shell-quote, while preserving the public BashTool API and adding shell-quote as a dependency.

Sequence diagram for safe bash command execution via spawn

sequenceDiagram
  actor Caller
  participant BashTool
  participant ShellQuote
  participant ChildProcess

  Caller->>BashTool: execute(command, timeout?)
  BashTool->>BashTool: checkConfirmation()
  alt command startsWith cd
    BashTool->>BashTool: update currentDirectory
    BashTool-->>Caller: ToolResult (no external command)
  else normal command
    BashTool->>BashTool: runCommandSafely(command, options)
    BashTool->>ShellQuote: parse(command)
    ShellQuote-->>BashTool: argv tokens
    BashTool->>ChildProcess: spawn(cmd, args, cwd, shell=false)
    par capture stdout
      ChildProcess-->>BashTool: stdout data events
      BashTool->>BashTool: append to stdout buffer
    and capture stderr
      ChildProcess-->>BashTool: stderr data events
      BashTool->>BashTool: append to stderr buffer
    end
    BashTool->>BashTool: start timeout timer
    alt timeout reached
      BashTool->>ChildProcess: kill()
      ChildProcess-->>BashTool: close
      BashTool-->>Caller: error Command timed out
    else buffer exceeded
      BashTool->>ChildProcess: kill()
      ChildProcess-->>BashTool: close
      BashTool-->>Caller: error Command output exceeded buffer limit
    else normal completion
      ChildProcess-->>BashTool: close with exit code
      BashTool-->>Caller: ToolResult with stdout, stderr
    end
  end
Loading

Updated class diagram for BashTool with runCommandSafely

classDiagram
  class BashTool {
    - currentDirectory : string
    - confirmationService
    - runCommandSafely(command : string, options : cwd? string, timeout? number, maxBuffer? number) Promise~stdout:string, stderr:string~
    + execute(command : string, timeout : number = 30000) Promise~ToolResult~
  }

  class ConfirmationService {
    + getInstance() ConfirmationService
  }

  class ToolResult

  class ShellQuote {
    + parse(command : string) any[]
  }

  class ChildProcessSpawn {
    + spawn(cmd : string, args : string[], options : cwd? string, shell? boolean)
  }

  BashTool --> ConfirmationService : uses
  BashTool --> ToolResult : returns
  BashTool --> ShellQuote : uses parse
  BashTool --> ChildProcessSpawn : uses spawn
Loading

File-Level Changes

Change Details Files
Introduce a safe spawn-based command execution helper and wire BashTool to use it instead of execAsync.
  • Import spawn from child_process and shell-quote for command parsing, removing the exec-based async helper in favor of a generic promisify alias kept for potential future use.
  • Add a private runCommandSafely(command, options) method that parses the command string via shell-quote.parse, filters to string tokens, derives cmd and args, and returns empty output on empty commands.
  • Inside runCommandSafely, spawn the child process with shell disabled, accumulate stdout/stderr into strings, enforce timeout via setTimeout and killing the process, and enforce maxBuffer by tracking accumulated output and killing the process if exceeded, rejecting with descriptive errors when limits are hit.
  • Update BashTool.execute to call runCommandSafely with the current working directory, timeout, and a 1MB maxBuffer, preserving existing confirmation and currentDirectory logic.
src/tools/bash.ts
Add shell-quote as a runtime dependency.
  • Declare shell-quote with a pinned version in the dependencies block to support robust shell-style command parsing at runtime.
package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 31, 2026

⚠️ No Changeset found

Latest commit: b1a02e3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@involvex involvex marked this pull request as ready for review January 31, 2026 01:48
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 security issue, 2 other issues, and left some high level feedback:

Security issues:

  • Detected calls to child_process from a function argument command. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)

General comments:

  • The runCommandSafely helper filters shell-quote.parse output down to only string tokens, which will silently drop things like redirections, pipes, or env assignments; consider explicitly detecting non-string tokens and failing with a clear error rather than changing the behavior of such commands in a non-obvious way.
  • The buffer enforcement now tracks stdout and stderr separately, whereas exec's maxBuffer applies to combined output; if you need to preserve original semantics, you may want to track total output length across both streams instead of per-stream.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `runCommandSafely` helper filters `shell-quote.parse` output down to only string tokens, which will silently drop things like redirections, pipes, or env assignments; consider explicitly detecting non-string tokens and failing with a clear error rather than changing the behavior of such commands in a non-obvious way.
- The buffer enforcement now tracks `stdout` and `stderr` separately, whereas `exec`'s `maxBuffer` applies to combined output; if you need to preserve original semantics, you may want to track total output length across both streams instead of per-stream.

## Individual Comments

### Comment 1
<location> `src/tools/bash.ts:73-75` </location>
<code_context>
+        reject(error);
+      });
+
+      child.on("close", code => {
+        clearTimeout(timer);
+        if (killedForTimeout) {
+          return reject(new Error("Command timed out"));
+        }
</code_context>

<issue_to_address>
**issue (bug_risk):** Non-zero exit codes no longer cause a rejected promise, changing error handling semantics vs. `exec`.

Previously, `execAsync` rejected on non-zero exit codes, allowing callers to use `try/catch` for failures. Here the promise resolves regardless of `code` (unless killed/errored), so failing commands can appear successful. Please add an explicit `code !== 0` check in the `close` handler and reject with an `Error` that includes the exit code (and maybe truncated `stderr`) to preserve the prior behavior and avoid regressions.
</issue_to_address>

### Comment 2
<location> `src/tools/bash.ts:13` </location>
<code_context>
   private currentDirectory: string = process.cwd();
   private confirmationService = ConfirmationService.getInstance();

+  private async runCommandSafely(
+    command: string,
+    options: { cwd?: string; timeout?: number; maxBuffer?: number },
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the custom spawn-based implementation of runCommandSafely with promisified execFile to keep safety guarantees while simplifying the control flow and resource handling logic.

You can keep the “safer command handling” (no shell, timeout, maxBuffer) while dropping most of the custom exec-like implementation by switching to `execFile` instead of manual `spawn` wiring.

This removes:
- manual stdout/stderr buffering
- explicit `maxBuffer` / `timeout` management
- `killedForTimeout` / `killedForBuffer` flags and custom timer
- the unused `promisifyFn` alias

and keeps:
- no shell invocation
- `timeout` and `maxBuffer` behavior
- `command: string` API with `shell-quote` parsing

Example refactor:

```ts
import { spawn, execFile } from "child_process"; // or drop `spawn` if unused
import { promisify } from "util";
import * as shellQuote from "shell-quote";

const execFileAsync = promisify(execFile);

export class BashTool {
  // ...

  private async runCommandSafely(
    command: string,
    options: { cwd?: string; timeout?: number; maxBuffer?: number },
  ): Promise<{ stdout: string; stderr: string }> {
    const argv = shellQuote
      .parse(command)
      .filter(token => typeof token === "string") as string[];

    if (argv.length === 0) {
      return { stdout: "", stderr: "" };
    }

    const [file, ...args] = argv;

    // execFile does no shell, and honors timeout/maxBuffer
    const { stdout, stderr } = await execFileAsync(file, args, {
      cwd: options.cwd,
      timeout: options.timeout ?? 30000,
      maxBuffer: options.maxBuffer ?? 1024 * 1024,
    });

    return { stdout, stderr };
  }
}
```

This preserves the new safety behavior and resource limits but substantially reduces control-flow and implementation complexity.
</issue_to_address>

### Comment 3
<location> `src/tools/bash.ts:30` </location>
<code_context>
      const child = spawn(cmd, args, {
</code_context>

<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `command`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/tools/bash.ts
Comment on lines +73 to +75
child.on("close", code => {
clearTimeout(timer);
if (killedForTimeout) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Non-zero exit codes no longer cause a rejected promise, changing error handling semantics vs. exec.

Previously, execAsync rejected on non-zero exit codes, allowing callers to use try/catch for failures. Here the promise resolves regardless of code (unless killed/errored), so failing commands can appear successful. Please add an explicit code !== 0 check in the close handler and reject with an Error that includes the exit code (and maybe truncated stderr) to preserve the prior behavior and avoid regressions.

Comment thread src/tools/bash.ts
private currentDirectory: string = process.cwd();
private confirmationService = ConfirmationService.getInstance();

private async runCommandSafely(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider replacing the custom spawn-based implementation of runCommandSafely with promisified execFile to keep safety guarantees while simplifying the control flow and resource handling logic.

You can keep the “safer command handling” (no shell, timeout, maxBuffer) while dropping most of the custom exec-like implementation by switching to execFile instead of manual spawn wiring.

This removes:

  • manual stdout/stderr buffering
  • explicit maxBuffer / timeout management
  • killedForTimeout / killedForBuffer flags and custom timer
  • the unused promisifyFn alias

and keeps:

  • no shell invocation
  • timeout and maxBuffer behavior
  • command: string API with shell-quote parsing

Example refactor:

import { spawn, execFile } from "child_process"; // or drop `spawn` if unused
import { promisify } from "util";
import * as shellQuote from "shell-quote";

const execFileAsync = promisify(execFile);

export class BashTool {
  // ...

  private async runCommandSafely(
    command: string,
    options: { cwd?: string; timeout?: number; maxBuffer?: number },
  ): Promise<{ stdout: string; stderr: string }> {
    const argv = shellQuote
      .parse(command)
      .filter(token => typeof token === "string") as string[];

    if (argv.length === 0) {
      return { stdout: "", stderr: "" };
    }

    const [file, ...args] = argv;

    // execFile does no shell, and honors timeout/maxBuffer
    const { stdout, stderr } = await execFileAsync(file, args, {
      cwd: options.cwd,
      timeout: options.timeout ?? 30000,
      maxBuffer: options.maxBuffer ?? 1024 * 1024,
    });

    return { stdout, stderr };
  }
}

This preserves the new safety behavior and resource limits but substantially reduces control-flow and implementation complexity.

Comment thread src/tools/bash.ts
const args = argv.slice(1);

return await new Promise((resolve, reject) => {
const child = spawn(cmd, args, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument command. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

@involvex involvex merged commit df6bb59 into main Jan 31, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant