What happens
packages/angular/cli/src/commands/update/utilities/git.ts:19:
function execGit(args: string[], input?: string): string {
return execFileSync('git', args, { encoding: 'utf8', stdio: 'pipe', input });
}
A bare command name and no cwd option, so the child inherits the process working directory — which
is the project the developer is standing in.
It is reached during argument parsing, before the command body runs.
packages/angular/cli/src/commands/update/cli.ts:140 sits inside a yargs .check():
if (packages?.length && !checkCleanGit(this.context.root)) {
so ng update <anything> calls it.
Why execFileSync does not prevent this
execFileSync uses no shell, which is usually where the analysis stops. On Windows it does not help:
CreateProcess searches the current directory as part of its default resolution order, so a bare
name can resolve against the project before PATH. No shell is involved; the only difference is that
the file must be .exe rather than .cmd.
NoDefaultCurrentDirectoryInExePath is the Windows mitigation for exactly this and is unset by
default.
Isolated, with a control:
execFileSync('git', ['-e', '<script>'], {cwd: plantedDir}) shell:false
-> canary present: true
-> *** PLANTED-git.exe-RAN
control, from a directory with no plant:
-> real git says: git version 2.55.0.windows.3
Executed against the published CLI
A real workspace created with npx @angular/cli@latest new victim --defaults --skip-git, then
node.exe copied to victim/git.exe. Because the CLI calls
execFileSync('git', ['rev-parse', '--show-toplevel']) and Node treats an unknown first argument as
a script path, a file named rev-parse was placed beside the plant — that is what makes the
execution observable, and it is the only contrivance in the test.
run : npx -y @angular/cli@latest update @angular/core (cwd = the project)
exit: 0
tail: Package '@angular/core' is already up to date.
*** PLANTED git.exe EXECUTED BY ng update ***
PLANTED-git.exe-RAN-VIA-ng-update 2026-08-03T11:43:44.846Z
The command reported success — it printed "already up to date" and exited 0, so nothing in the
output indicates that a binary from the project directory ran.
Scope, stated precisely
- Windows only. POSIX
execFileSync uses execvp, whose PATH does not include ..
- What was measured is code execution as the developer, nothing beyond it. I did not execute any
follow-on step, so I am not claiming credential theft here — only that an executable supplied by
the project runs, silently, as the user.
- The delivery is a repository the developer has cloned and chosen to work in — a template, a bug
reproduction, a course project, a pull-request branch. That is a real precondition and it is
stated rather than argued away.
packages/angular/cli/src/utilities/completion.ts:274 has the same shape — an execFile with a bare
name — and would be worth fixing alongside.
Suggested fix
Resolve the tool once from PATH only, require the result to be absolute, fail closed if it is not
found, and pass that absolute path to execFileSync instead of the bare name. Node has no built-in
"PATH only" lookup, so this is a small helper rather than a flag; the property that matters is that
no value derived from the current directory is ever executed.
angular/dev-infra merged the same shape of fix for a different binary in PR #3753, "validate
.nvmrc and resolved node path in invokeNvmInstall".
Prior art checked
Tracker searches for execFileSync git, binary planting, current directory git.exe and CWE-427
returned nothing relevant. The nearest result is #32134, "refactor(@angular/cli): standardize
update command git utility" — closed — which reorganised this file without changing how the command
name is resolved. CHANGELOG.md has no matching entry.
Disclosure
Reported to Google's OSS VRP. They assessed it as below the threshold they use for escalating to
product teams and explicitly invited public disclosure, which is why this is filed here.
The weakness class is CWE-427, Uncontrolled Search Path Element. It is not a Node defect and not a
Windows quirk — the fix is a few lines in this repository.
What happens
packages/angular/cli/src/commands/update/utilities/git.ts:19:A bare command name and no
cwdoption, so the child inherits the process working directory — whichis the project the developer is standing in.
It is reached during argument parsing, before the command body runs.
packages/angular/cli/src/commands/update/cli.ts:140sits inside a yargs.check():so
ng update <anything>calls it.Why
execFileSyncdoes not prevent thisexecFileSyncuses no shell, which is usually where the analysis stops. On Windows it does not help:CreateProcesssearches the current directory as part of its default resolution order, so a barename can resolve against the project before
PATH. No shell is involved; the only difference is thatthe file must be
.exerather than.cmd.NoDefaultCurrentDirectoryInExePathis the Windows mitigation for exactly this and is unset bydefault.
Isolated, with a control:
Executed against the published CLI
A real workspace created with
npx @angular/cli@latest new victim --defaults --skip-git, thennode.execopied tovictim/git.exe. Because the CLI callsexecFileSync('git', ['rev-parse', '--show-toplevel'])and Node treats an unknown first argument asa script path, a file named
rev-parsewas placed beside the plant — that is what makes theexecution observable, and it is the only contrivance in the test.
The command reported success — it printed "already up to date" and exited 0, so nothing in the
output indicates that a binary from the project directory ran.
Scope, stated precisely
execFileSyncusesexecvp, whosePATHdoes not include..follow-on step, so I am not claiming credential theft here — only that an executable supplied by
the project runs, silently, as the user.
reproduction, a course project, a pull-request branch. That is a real precondition and it is
stated rather than argued away.
packages/angular/cli/src/utilities/completion.ts:274has the same shape — anexecFilewith a barename — and would be worth fixing alongside.
Suggested fix
Resolve the tool once from
PATHonly, require the result to be absolute, fail closed if it is notfound, and pass that absolute path to
execFileSyncinstead of the bare name. Node has no built-in"PATH only" lookup, so this is a small helper rather than a flag; the property that matters is that
no value derived from the current directory is ever executed.
angular/dev-inframerged the same shape of fix for a different binary in PR #3753, "validate.nvmrc and resolved node path in invokeNvmInstall".
Prior art checked
Tracker searches for
execFileSync git,binary planting,current directory git.exeandCWE-427returned nothing relevant. The nearest result is #32134, "refactor(@angular/cli): standardize
update command git utility" — closed — which reorganised this file without changing how the command
name is resolved.
CHANGELOG.mdhas no matching entry.Disclosure
Reported to Google's OSS VRP. They assessed it as below the threshold they use for escalating to
product teams and explicitly invited public disclosure, which is why this is filed here.
The weakness class is CWE-427, Uncontrolled Search Path Element. It is not a Node defect and not a
Windows quirk — the fix is a few lines in this repository.