Skip to content

ng update resolves git as a bare name with no cwd, so on Windows it executes git.exe from the project directory #33755

Description

@bilguunbicktivism

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 angular/dev-infra#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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions