-
-
Notifications
You must be signed in to change notification settings - Fork 1
Potential fix for code scanning alert no. 11: Uncontrolled command line #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,88 @@ | ||
| import { ConfirmationService } from "../utils/confirmation-service"; | ||
| import { ToolResult } from "../types/index"; | ||
| import { exec } from "child_process"; | ||
| import { spawn } from "child_process"; | ||
| import { promisify } from "util"; | ||
| import * as shellQuote from "shell-quote"; | ||
|
|
||
| const execAsync = promisify(exec); | ||
| const promisifyFn = promisify; // kept in case other parts of this file evolve to use promisify | ||
|
|
||
| export class BashTool { | ||
| private currentDirectory: string = process.cwd(); | ||
| private confirmationService = ConfirmationService.getInstance(); | ||
|
|
||
| private async runCommandSafely( | ||
| command: string, | ||
| options: { cwd?: string; timeout?: number; maxBuffer?: number }, | ||
| ): Promise<{ stdout: string; stderr: string }> { | ||
| const parsed = shellQuote.parse(command); | ||
| const argv = parsed | ||
| .filter(token => typeof token === "string") | ||
| .map(token => token as string); | ||
|
|
||
| if (argv.length === 0) { | ||
| return { stdout: "", stderr: "" }; | ||
| } | ||
|
|
||
| const cmd = argv[0]; | ||
| const args = argv.slice(1); | ||
|
|
||
| return await new Promise((resolve, reject) => { | ||
| const child = spawn(cmd, args, { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Source: opengrep |
||
| cwd: options.cwd, | ||
| shell: false, | ||
| }); | ||
|
|
||
| let stdout = ""; | ||
| let stderr = ""; | ||
| const maxBuffer = options.maxBuffer ?? 1024 * 1024; | ||
| const timeout = options.timeout ?? 30000; | ||
|
|
||
| let killedForTimeout = false; | ||
| let killedForBuffer = false; | ||
|
|
||
| const timer = setTimeout(() => { | ||
| killedForTimeout = true; | ||
| child.kill(); | ||
| }, timeout); | ||
|
|
||
| child.stdout?.on("data", (data: Buffer | string) => { | ||
| const chunk = data.toString(); | ||
| if (stdout.length + chunk.length > maxBuffer) { | ||
| killedForBuffer = true; | ||
| child.kill(); | ||
| return; | ||
| } | ||
| stdout += chunk; | ||
| }); | ||
|
|
||
| child.stderr?.on("data", (data: Buffer | string) => { | ||
| const chunk = data.toString(); | ||
| if (stderr.length + chunk.length > maxBuffer) { | ||
| killedForBuffer = true; | ||
| child.kill(); | ||
| return; | ||
| } | ||
| stderr += chunk; | ||
| }); | ||
|
|
||
| child.on("error", error => { | ||
| clearTimeout(timer); | ||
| reject(error); | ||
| }); | ||
|
|
||
| child.on("close", code => { | ||
| clearTimeout(timer); | ||
| if (killedForTimeout) { | ||
|
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Previously, |
||
| return reject(new Error("Command timed out")); | ||
| } | ||
| if (killedForBuffer) { | ||
| return reject(new Error("Command output exceeded buffer limit")); | ||
| } | ||
| resolve({ stdout, stderr }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| async execute(command: string, timeout: number = 30000): Promise<ToolResult> { | ||
| try { | ||
| // Check if user has already accepted bash commands for this session | ||
|
|
@@ -53,7 +127,7 @@ export class BashTool { | |
| } | ||
| } | ||
|
|
||
| const { stdout, stderr } = await execAsync(command, { | ||
| const { stdout, stderr } = await this.runCommandSafely(command, { | ||
| cwd: this.currentDirectory, | ||
| timeout, | ||
| maxBuffer: 1024 * 1024, | ||
|
|
||
There was a problem hiding this comment.
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
execFileinstead of manualspawnwiring.This removes:
maxBuffer/timeoutmanagementkilledForTimeout/killedForBufferflags and custom timerpromisifyFnaliasand keeps:
timeoutandmaxBufferbehaviorcommand: stringAPI withshell-quoteparsingExample refactor:
This preserves the new safety behavior and resource limits but substantially reduces control-flow and implementation complexity.