Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions packages/core/src/ide/ide-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,44 @@ async function findCommand(
command: string,
platform: NodeJS.Platform = process.platform,
): Promise<string | null> {
// 1. Check PATH first.
// Security: Validate the command name to prevent injection attacks.
// Only allow alphanumeric characters, hyphens, underscores, dots, and path separators.
const safeCommandPattern = /^[a-zA-Z0-9.\-_/\\]+$/;
if (!safeCommandPattern.test(command)) {
return null;
}

// 1. Check PATH first using safe argument passing (no shell interpolation).
try {
if (platform === 'win32') {
const result = child_process
.execSync(`where.exe ${command}`)
.toString()
.trim();
// `where.exe` can return multiple paths. Return the first one.
const firstPath = result.split(/\r?\n/)[0];
if (firstPath) {
return firstPath;
.spawnSync('where.exe', [command], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
});
if (result.status === 0 && result.stdout) {
const firstPath = result.stdout.trim().split(/\r?\n/)[0];
if (firstPath) {
// Sanitize: resolve to absolute path and reject null bytes
// to prevent path traversal or null byte injection from
// untrusted command output.
const resolved = path.resolve(firstPath);
if (resolved.includes('\0')) {
return null;
}
return resolved;
}
}
Comment on lines +44 to 56
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.

security-high high

The path returned by where.exe is extracted from command output, which is considered an untrusted source. To prevent potential path traversal (..) or null byte injection (\0) vulnerabilities, the path should be sanitized and resolved before being returned and executed, in accordance with our security rules.

Suggested change
if (result.status === 0 && result.stdout) {
const firstPath = result.stdout.trim().split(/\r?\n/)[0];
if (firstPath) {
return firstPath;
}
}
if (result.status === 0 && result.stdout) {
const firstPath = result.stdout.trim().split(/\r?\n/)[0];
if (firstPath && !firstPath.includes('\0') && !firstPath.includes('..')) {
return path.resolve(firstPath);
}
}
References
  1. Sanitize file paths extracted from untrusted sources, such as command output, to prevent path traversal (..), null byte injection (\0), and other vulnerabilities.

} else {
child_process.execSync(`command -v ${command}`, {
stdio: 'ignore',
const result = child_process.spawnSync('which', [command], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
});
return command;
if (result.status === 0) {
return command;
}
}
} catch {
// Not in PATH, continue to check common locations.
Expand Down
44 changes: 26 additions & 18 deletions packages/core/src/utils/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { exec, execSync, spawn, spawnSync } from 'node:child_process';
import { promisify } from 'node:util';
import { spawn, spawnSync } from 'node:child_process';
import { once } from 'node:events';
import { debugLogger } from './debugLogger.js';
import { coreEvents, CoreEvent, type EditorSelectedPayload } from './events.js';
Expand Down Expand Up @@ -102,30 +101,39 @@ interface DiffCommand {
args: string[];
}

const execAsync = promisify(exec);

function getCommandExistsCmd(cmd: string): string {
return process.platform === 'win32'
? `where.exe ${cmd}`
: `command -v ${cmd}`;
}

function commandExists(cmd: string): boolean {
try {
execSync(getCommandExistsCmd(cmd), { stdio: 'ignore' });
return true;
// Security: Use spawnSync with argument arrays (shell: false) to prevent
// command injection via shell metacharacters in the cmd parameter.
if (process.platform === 'win32') {
const result = spawnSync('where.exe', [cmd], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
});
return result.status === 0;
} else {
const result = spawnSync('which', [cmd], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: false,
});
return result.status === 0;
}
} catch {
return false;
}
}

async function commandExistsAsync(cmd: string): Promise<boolean> {
try {
await execAsync(getCommandExistsCmd(cmd));
return true;
} catch {
return false;
}
return new Promise<boolean>((resolve) => {
// Security: Use spawn with argument arrays (shell: false) to prevent
// command injection via shell metacharacters in the cmd parameter.
const bin = process.platform === 'win32' ? 'where.exe' : 'which';
const child = spawn(bin, [cmd], {
stdio: ['pipe', 'pipe', 'pipe'],
});
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}

/**
Expand Down