diff --git a/packages/cli/src/utils/handleAutoUpdate.test.ts b/packages/cli/src/utils/handleAutoUpdate.test.ts index bb88baf51..d448b9e7b 100644 --- a/packages/cli/src/utils/handleAutoUpdate.test.ts +++ b/packages/cli/src/utils/handleAutoUpdate.test.ts @@ -241,9 +241,12 @@ describe('handleAutoUpdate', () => { handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn); expect(mockSpawn).toHaveBeenCalledWith( - 'npm i -g @qwen-code/qwen-code@nightly', + expect.stringMatching(/^(bash|cmd\.exe)$/), + expect.arrayContaining([ + expect.stringMatching(/^(-c|\/c)$/), + 'npm i -g @qwen-code/qwen-code@nightly', + ]), { - shell: true, stdio: 'pipe', }, ); diff --git a/packages/cli/src/utils/handleAutoUpdate.ts b/packages/cli/src/utils/handleAutoUpdate.ts index a0691a0c6..21ff7be63 100644 --- a/packages/cli/src/utils/handleAutoUpdate.ts +++ b/packages/cli/src/utils/handleAutoUpdate.ts @@ -12,6 +12,7 @@ import type { HistoryItem } from '../ui/types.js'; import { MessageType } from '../ui/types.js'; import { spawnWrapper } from './spawnWrapper.js'; import type { spawn } from 'node:child_process'; +import os from 'node:os'; export function handleAutoUpdate( info: UpdateObject | null, @@ -53,7 +54,10 @@ export function handleAutoUpdate( '@latest', isNightly ? '@nightly' : `@${info.update.latest}`, ); - const updateProcess = spawnFn(updateCommand, { stdio: 'pipe', shell: true }); + const isWindows = os.platform() === 'win32'; + const shell = isWindows ? 'cmd.exe' : 'bash'; + const shellArgs = isWindows ? ['/c', updateCommand] : ['-c', updateCommand]; + const updateProcess = spawnFn(shell, shellArgs, { stdio: 'pipe' }); let errorOutput = ''; updateProcess.stderr.on('data', (data) => { errorOutput += data.toString(); diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 749d7193e..71f5c47d8 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -291,9 +291,10 @@ export async function start_sandbox( sandboxEnv['NO_PROXY'] = noProxy; sandboxEnv['no_proxy'] = noProxy; } - proxyProcess = spawn(proxyCommand, { + // Note: CodeQL flags this as js/shell-command-injection-from-environment. + // This is intentional - CLI tool executes user-provided proxy commands. + proxyProcess = spawn('bash', ['-c', proxyCommand], { stdio: ['ignore', 'pipe', 'pipe'], - shell: true, detached: true, }); // install handlers to stop proxy on exit/signal @@ -781,9 +782,15 @@ export async function start_sandbox( if (proxyCommand) { // run proxyCommand in its own container const proxyContainerCommand = `${config.command} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`; - proxyProcess = spawn(proxyContainerCommand, { + const isWindows = os.platform() === 'win32'; + const proxyShell = isWindows ? 'cmd.exe' : 'bash'; + const proxyShellArgs = isWindows + ? ['/c', proxyContainerCommand] + : ['-c', proxyContainerCommand]; + // Note: CodeQL flags this as js/shell-command-injection-from-environment. + // This is intentional - CLI tool executes user-provided proxy commands in container. + proxyProcess = spawn(proxyShell, proxyShellArgs, { stdio: ['ignore', 'pipe', 'pipe'], - shell: true, detached: true, }); // install handlers to stop proxy on exit/signal diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts index b598757a7..8c8e7bd4a 100644 --- a/packages/core/src/services/shellExecutionService.test.ts +++ b/packages/core/src/services/shellExecutionService.test.ts @@ -580,9 +580,11 @@ describe('ShellExecutionService child_process fallback', () => { }); expect(mockCpSpawn).toHaveBeenCalledWith( - 'ls -l', - [], - expect.objectContaining({ shell: 'bash' }), + 'bash', + ['-c', 'ls -l'], + expect.objectContaining({ + detached: true, + }), ); expect(result.exitCode).toBe(0); expect(result.signal).toBeNull(); @@ -825,10 +827,9 @@ describe('ShellExecutionService child_process fallback', () => { ); expect(mockCpSpawn).toHaveBeenCalledWith( - 'dir "foo bar"', - [], + 'cmd.exe', + ['/c', 'dir "foo bar"'], expect.objectContaining({ - shell: true, detached: false, windowsHide: true, }), @@ -840,10 +841,9 @@ describe('ShellExecutionService child_process fallback', () => { await simulateExecution('ls "foo bar"', (cp) => cp.emit('exit', 0, null)); expect(mockCpSpawn).toHaveBeenCalledWith( - 'ls "foo bar"', - [], + 'bash', + ['-c', 'ls "foo bar"'], expect.objectContaining({ - shell: 'bash', detached: true, }), ); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 2346de1b2..3d812d899 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -223,12 +223,17 @@ export class ShellExecutionService { ): ShellExecutionHandle { try { const isWindows = os.platform() === 'win32'; + const shell = isWindows ? 'cmd.exe' : 'bash'; + const shellArgs = isWindows + ? ['/c', commandToExecute] + : ['-c', commandToExecute]; - const child = cpSpawn(commandToExecute, [], { + // Note: CodeQL flags this as js/shell-command-injection-from-environment. + // This is intentional - CLI tool executes user-provided shell commands. + const child = cpSpawn(shell, shellArgs, { cwd, stdio: ['ignore', 'pipe', 'pipe'], - windowsVerbatimArguments: true, - shell: isWindows ? true : 'bash', + windowsVerbatimArguments: isWindows, detached: !isWindows, windowsHide: isWindows, env: {