Conversation
| ): Promise<string | undefined> { | ||
| return new Promise((resolve, reject) => { | ||
| const child = cp.exec(cmd, opts, (error: ExecError | null, stdout?: string, stderr?: string) => { | ||
| const child = cp.exec(cmd, opts, (error, stdout, stderr) => { |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, the fix is to stop passing a fully constructed command string into cp.exec/cp.execSync when that string depends on environment or filesystem values, and instead call a no-shell API (cp.spawn / cp.execFile / cp.spawnSync) with the executable and arguments passed separately as an array.
For this codebase, the best targeted fix is:
- Change the generic
exechelper inpackages/just-scripts/src/utils/exec.tsto accept acmdandargsarray, and implement it usingcp.spawnrather thancp.exec. This avoids the shell entirely. - Update its TypeScript type signature accordingly, and keep the promise-based API and stdout/stderr piping behavior unchanged.
- Update the call sites in
tslintTask.tsandtscTask.tsthat currently build a singlecmdstring viaencodeArgs(...).join(' ')to instead:- Build an argument array
[process.execPath, tslintCmd, ...args]or[process.execPath, ...args]. - Call the new
execasexec(process.execPath, args, ...)without usingencodeArgsor joining into a string. - Keep logging behavior; they can still log
encodeArgs([...]).join(' ')for display only.
- Build an argument array
- Leave
encodeArgsimplemented as-is for logging or any other callers that may still want shell-style strings; just don’t use it to build the command that is actually executed in these tasks.
Concretely:
-
In
packages/just-scripts/src/utils/exec.ts, modifyexecto:export function exec( cmd: string, args: ReadonlyArray<string> = [], opts: cp.SpawnOptions & { stdout?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream } = {}, ): Promise<string | undefined> { // implement using cp.spawn, buffer stdout/stderr when not piped, etc. }
and replace the current
cp.exec-based implementation with acp.spawn-based one that:- Pipes
child.stdout/child.stderrinto the provided streams when present. - Otherwise, collects them into buffers/strings so the promise can resolve with the stdout string or reject with an
ExecErrorcontainingstdoutandstderr. - Honors
optsfor cwd, env, etc. (forward them tocp.spawn).
- Pipes
-
In
tslintTask.ts:- Replace
const cmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' ');with separatecmdArgs = [tslintCmd, ...args];. - Keep
logger.infologging a shell-like representation for humans:logger.info(encodeArgs([process.execPath, tslintCmd, ...args]).join(' '));. - Call
return exec(process.execPath, [tslintCmd, ...args], { stdout: process.stdout, stderr: process.stderr });.
- Replace
-
In
tscTask.ts:- For
tscTask, keepargs = argsFromOptions(tscCmd, options);as-is but:- Log with
encodeArgs([process.execPath, ...args]).join(' ');. - Call
return exec(process.execPath, args);.
- Log with
tscWatchTaskalready usesspawn(process.execPath, cmd, ...)with an argument array, which is safe, so no change is required there.
- For
No new helper methods or imports are required beyond reusing the existing cp.spawn import and the already-exported spawn helper; we only adjust types and logic within the shown snippets.
| @@ -25,9 +25,9 @@ | ||
| logger.info(`Running ${tscCmd} with ${options.project || options.build}`); | ||
|
|
||
| const args = argsFromOptions(tscCmd, options); | ||
| const cmd = encodeArgs([process.execPath, ...args]).join(' '); | ||
| logger.info(`Executing: ${cmd}`); | ||
| return exec(cmd); | ||
| const displayCmd = encodeArgs([process.execPath, ...args]).join(' '); | ||
| logger.info(`Executing: ${displayCmd}`); | ||
| return exec(process.execPath, args); | ||
| } | ||
| return Promise.resolve(); | ||
| }; |
| @@ -31,9 +31,9 @@ | ||
| args.push('--fix'); | ||
| } | ||
|
|
||
| const cmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' '); | ||
| logger.info(cmd); | ||
| return exec(cmd, { stdout: process.stdout, stderr: process.stderr }); | ||
| const displayCmd = encodeArgs([process.execPath, tslintCmd, ...args]).join(' '); | ||
| logger.info(displayCmd); | ||
| return exec(process.execPath, [tslintCmd, ...args], { stdout: process.stdout, stderr: process.stderr }); | ||
| } else { | ||
| return Promise.resolve(); | ||
| } |
Overview
Test Notes