Command
add, new
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Two files in Angular CLI use a deprecated spawn() pattern on Windows — arguments are concatenated into a single string instead of being passed as an array. This causes shell metacharacters (&, |, >) to be interpreted by cmd.exe, leading to unintended OS command injection.
Node.js has explicitly deprecated this pattern as DEP0190. The Linux/macOS code path already does it correctly.
File 1: packages/angular/cli/src/package-managers/host.ts (Lines 135-136)
const childProcess = isWin32
? spawn(`${command} ${args.join(' ')}`, spawnOptions) // Windows: concatenated string
: spawn(command, args, spawnOptions); // Linux: proper array
When ng add receives a package specifier with shell metacharacters (e.g., lodash@1.0.0 & echo injected), npm-package-arg parses it as a valid range specifier. The & echo injected portion flows unsanitized into args, gets concatenated, and cmd.exe executes it as a separate command.
File 2: packages/angular_devkit/schematics/tasks/repo-init/executor.ts (Line 44)
spawn(`git ${args.join(' ')}`, spawnOptions); // Same deprecated pattern
The git commit message passed via ng new --commit.message="..." flows into this spawn() call. A crafted message like " & echo PWNED > pwned.txt & rem " breaks out of the git command and executes arbitrary commands.
The commit message is injected at line 85 without sanitization:
await execute(['commit', `-m "${message}"`]);
Impact: On Windows, both code paths allow OS command injection — arbitrary commands execute with the privileges of the user running Angular CLI. This is a behavioral inconsistency with Linux/macOS, where args are passed as an array and shell metacharacters are treated as literal characters.
Minimal Reproduction
Reproduction 1: ng add (host.ts)
ng new test-app
cd test-app
ng add "lodash@1.0.0 & echo INJECTED" --skip-confirmation
Expected: The entire string lodash@1.0.0 & echo INJECTED is treated as a single argument (same as Linux).
Actual (Windows): cmd.exe splits at & and runs echo INJECTED as a separate command.
Reproduction 2: ng new (executor.ts)
ng new malicious-app --skip-install --commit.message="\" & echo PWNED > pwned.txt & rem \""
Expected: The message is passed as a literal string to git commit -m.
Actual (Windows): cmd.exe executes echo PWNED > pwned.txt — a file pwned.txt is created in the working directory, confirming OS command injection.
Exception or Error
No exception. The injected commands execute silently alongside the intended operation. This is what makes the behavior dangerous — there is no visible error to alert the user.
Example for `ng add`:
✔ Determining Package Manager
› Using package manager: npm
✔ Loading package information ← injected command runs here silently
✔ Installing package
Your Environment
Angular CLI: 21.1.4
Node: 25.6.0
Package Manager: npm 11.3.0
OS: Windows 11
Angular: 21.1.4
Package Version
------------------------------------------------------
@angular-devkit/architect 0.2101.4
@angular-devkit/build-angular 21.1.4
@angular-devkit/core 21.1.4
@angular-devkit/schematics 21.1.4
@angular/cli 21.1.4
@schematics/angular 21.1.4
Anything else relevant?
Suggested fix for both files — use the same spawn pattern as Linux/macOS:
File 1 (host.ts):
- const childProcess = isWin32
- ? spawn(`${command} ${args.join(' ')}`, spawnOptions)
- : spawn(command, args, spawnOptions);
+ const childProcess = spawn(command, args, spawnOptions);
File 2 (executor.ts):
- spawn(`git ${args.join(' ')}`, spawnOptions)
+ spawn('git', args, spawnOptions)
And for the commit message argument:
- await execute(['commit', `-m "${message}"`]);
+ await execute(['commit', '-m', message]);
Real-World Attack Scenarios
1. Malicious npm package README / Installation instructions`
2. Open-source project contributor exploitation
3. CI/CD pipeline poisoning
References:
Command
add, new
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Two files in Angular CLI use a deprecated
spawn()pattern on Windows — arguments are concatenated into a single string instead of being passed as an array. This causes shell metacharacters (&,|,>) to be interpreted bycmd.exe, leading to unintended OS command injection.Node.js has explicitly deprecated this pattern as DEP0190. The Linux/macOS code path already does it correctly.
File 1:
packages/angular/cli/src/package-managers/host.ts(Lines 135-136)When
ng addreceives a package specifier with shell metacharacters (e.g.,lodash@1.0.0 & echo injected),npm-package-argparses it as a valid range specifier. The& echo injectedportion flows unsanitized intoargs, gets concatenated, andcmd.exeexecutes it as a separate command.File 2:
packages/angular_devkit/schematics/tasks/repo-init/executor.ts(Line 44)The git commit message passed via
ng new --commit.message="..."flows into thisspawn()call. A crafted message like" & echo PWNED > pwned.txt & rem "breaks out of the git command and executes arbitrary commands.The commit message is injected at line 85 without sanitization:
Impact: On Windows, both code paths allow OS command injection — arbitrary commands execute with the privileges of the user running Angular CLI. This is a behavioral inconsistency with Linux/macOS, where args are passed as an array and shell metacharacters are treated as literal characters.
Minimal Reproduction
Reproduction 1:
ng add(host.ts)Expected: The entire string
lodash@1.0.0 & echo INJECTEDis treated as a single argument (same as Linux).Actual (Windows):
cmd.exesplits at&and runsecho INJECTEDas a separate command.Reproduction 2:
ng new(executor.ts)ng new malicious-app --skip-install --commit.message="\" & echo PWNED > pwned.txt & rem \""Expected: The message is passed as a literal string to
git commit -m.Actual (Windows):
cmd.exeexecutesecho PWNED > pwned.txt— a filepwned.txtis created in the working directory, confirming OS command injection.Exception or Error
Your Environment
Anything else relevant?
Suggested fix for both files — use the same spawn pattern as Linux/macOS:
File 1 (
host.ts):File 2 (
executor.ts):And for the commit message argument:
Real-World Attack Scenarios
1. Malicious npm package README / Installation instructions`
2. Open-source project contributor exploitation
3. CI/CD pipeline poisoning
References: