diff --git a/apps/frontend/src/main/cli-tool-manager.ts b/apps/frontend/src/main/cli-tool-manager.ts index ef09b62563..1a1b69b007 100644 --- a/apps/frontend/src/main/cli-tool-manager.ts +++ b/apps/frontend/src/main/cli-tool-manager.ts @@ -20,7 +20,7 @@ * - Graceful fallbacks when tools not found */ -import { execFileSync, execFile } from 'child_process'; +import { execFileSync, execFile, type ExecFileOptionsWithStringEncoding, type ExecFileSyncOptions } from 'child_process'; import { existsSync, readdirSync, promises as fsPromises } from 'fs'; import path from 'path'; import os from 'os'; @@ -32,10 +32,10 @@ import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-p const execFileAsync = promisify(execFile); -type ExecFileSyncOptionsWithVerbatim = import('child_process').ExecFileSyncOptions & { +export type ExecFileSyncOptionsWithVerbatim = ExecFileSyncOptions & { windowsVerbatimArguments?: boolean; }; -type ExecFileAsyncOptionsWithVerbatim = import('child_process').ExecFileOptionsWithStringEncoding & { +export type ExecFileAsyncOptionsWithVerbatim = ExecFileOptionsWithStringEncoding & { windowsVerbatimArguments?: boolean; }; diff --git a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts index 28f912d6c0..e257bd6339 100644 --- a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts @@ -16,7 +16,7 @@ import { promisify } from 'util'; import { IPC_CHANNELS, DEFAULT_APP_SETTINGS } from '../../shared/constants'; import type { IPCResult } from '../../shared/types'; import type { ClaudeCodeVersionInfo, ClaudeInstallationList, ClaudeInstallationInfo } from '../../shared/types/cli'; -import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths } from '../cli-tool-manager'; +import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths, type ExecFileAsyncOptionsWithVerbatim } from '../cli-tool-manager'; import { readSettingsFile, writeSettingsFile } from '../settings-utils'; import { isSecurePath } from '../utils/windows-paths'; import semver from 'semver'; @@ -38,6 +38,11 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string try { const isWindows = process.platform === 'win32'; + // Security validation: reject paths with shell metacharacters or directory traversal + if (isWindows && !isSecurePath(cliPath)) { + throw new Error(`Claude CLI path failed security validation: ${cliPath}`); + } + // Augment PATH with the CLI directory for proper resolution const cliDir = path.dirname(cliPath); const env = { @@ -56,12 +61,14 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string || path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe'); // Use double-quoted command line for paths with spaces const cmdLine = `""${cliPath}" --version"`; - const result = await execFileAsync(cmdExe, ['/d', '/s', '/c', cmdLine], { + const execOptions: ExecFileAsyncOptionsWithVerbatim = { encoding: 'utf-8', timeout: 5000, windowsHide: true, + windowsVerbatimArguments: true, env, - }); + }; + const result = await execFileAsync(cmdExe, ['/d', '/s', '/c', cmdLine], execOptions); stdout = result.stdout; } else { const result = await execFileAsync(cliPath, ['--version'], {