Skip to content
Open
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
21 changes: 19 additions & 2 deletions ng-dev/utils/child-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export abstract class ChildProcess {
* @returns The command's stdout and stderr.
*/
static spawnSync(command: string, args: string[], options: SpawnSyncOptions = {}): SpawnResult {
// Pass args as a proper array with shell: false to prevent OS command injection.
// When shell: true is used, Node.js internally joins command + args into a single
// string evaluated by /bin/sh, making shell metacharacters in args exploitable.
// Note: shell: false may affect .cmd/.bat execution on Windows, but ng-dev
// targets Linux/macOS CI environments where this is not a concern.
const commandText = `${command} ${args.join(' ')}`;
const env = getEnvironmentForNonInteractiveCommand(options.env);

Expand All @@ -95,7 +100,7 @@ export abstract class ChildProcess {
signal,
stdout,
stderr,
} = _spawnSync(command, args, {...options, env, encoding: 'utf8', shell: true, stdio: 'pipe'});
} = _spawnSync(command, args, {...options, env, encoding: 'utf8', shell: false, stdio: 'pipe'});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Setting shell: false is a significant security improvement. However, please be aware that this change may break the execution of batch files (.cmd, .bat) on Windows, as they require a shell to run. If Windows support is required for this tool, you might need to use a library like cross-spawn or explicitly handle the command path for Windows.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the follow-up commit. Added a comment to both spawn and spawnSync
clarifying that ng-dev targets Linux/macOS CI environments only, so .cmd/.bat
compatibility on Windows is not a concern here.


/** The status of the spawn result. */
const status = statusFromExitCodeAndSignal(exitCode, signal);
Expand All @@ -116,13 +121,18 @@ export abstract class ChildProcess {
* rejects on command failure.
*/
static spawn(command: string, args: string[], options: SpawnOptions = {}): Promise<SpawnResult> {
// Pass args as a proper array with shell: false to prevent OS command injection.
// When shell: true is used, Node.js internally joins command + args into a single
// string evaluated by /bin/sh, making shell metacharacters in args exploitable.
// Note: shell: false may affect .cmd/.bat execution on Windows, but ng-dev
// targets Linux/macOS CI environments where this is not a concern.
const commandText = `${command} ${args.join(' ')}`;
const env = getEnvironmentForNonInteractiveCommand(options.env);

return processAsyncCmd(
commandText,
options,
_spawn(command, args, {...options, env, shell: true, stdio: 'pipe'}),
_spawn(command, args, {...options, env, shell: false, stdio: 'pipe'}),
);
}

Expand All @@ -133,8 +143,15 @@ export abstract class ChildProcess {
*
* @returns a Promise resolving with captured stdout and stderr on success. The promise
* rejects on command failure.
* @deprecated Use ChildProcess.spawn with an explicit args array instead.
* exec() passes the command string directly to the shell, making it susceptible
* to command injection via shell metacharacters in the command string.
*/
static exec(command: string, options: ExecOptions = {}): Promise<SpawnResult> {
Log.warn(
`ChildProcess.exec is discouraged as it is susceptible to command injection ` +
`(command: ${command}). Prefer ChildProcess.spawn with an array of arguments.`,
);
const env = getEnvironmentForNonInteractiveCommand(options.env);
return processAsyncCmd(command, options, _exec(command, {...options, env}));
}
Expand Down
Loading