Skip to content

fix(@angular/cli): resolve executables strictly from PATH - #33758

Open
alan-agius4 wants to merge 2 commits into
angular:mainfrom
alan-agius4:fix-path-executable-resolution
Open

fix(@angular/cli): resolve executables strictly from PATH#33758
alan-agius4 wants to merge 2 commits into
angular:mainfrom
alan-agius4:fix-path-executable-resolution

Conversation

@alan-agius4

@alan-agius4alan-agius4 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Update executable invocation logic to resolve system binaries (such as git and which) strictly from the PATH environment variable.

This prevents bare command names passed to execFileSync / execFile from implicitly searching and resolving binaries relative to process.cwd() on Windows.

Fixes#33755

@angular-robotangular-robotBot added the area: build & ci Related the build and CI infrastructure of the project label Aug 5, 2026
gemini-code-assist[bot]

This comment was marked as outdated.

@alan-agius4alan-agius4 added action: review The PR is still awaiting reviews from at least one requested reviewer target: patch This PR is targeted for the next patch release labels Aug 5, 2026
@alan-agius4
alan-agius4force-pushed the fix-path-executable-resolution branch from 12b5aa7 to 62cbca6CompareAugust 5, 2026 06:23
@alan-agius4alan-agius4 changed the title ci: schedule ng-snapshot Renovate updates for early morning onlyfix(@angular/cli): resolve executables strictly from PATHAug 5, 2026
@alan-agius4

Copy link
Copy Markdown
CollaboratorAuthor

/gemini review

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown

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 introduces a utility function findExecutableOnPath to safely resolve executables from the PATH environment variable, preventing implicit resolution from the current working directory on Windows. It integrates this utility when executing git and which. The review feedback highlights critical security and correctness improvements: avoiding fallback to bare command names ('git' and 'which') when they are not found on the PATH to prevent command injection risks, and stripping double quotes from PATH directory entries on Windows to correctly handle paths with spaces.

Comment threadpackages/angular/cli/src/commands/update/utilities/git.ts Outdated
Comment threadpackages/angular/cli/src/utilities/completion.ts Outdated
Comment threadpackages/angular/cli/src/utilities/executable.ts Outdated
@alan-agius4
alan-agius4force-pushed the fix-path-executable-resolution branch from 62cbca6 to e60ed21CompareAugust 5, 2026 06:36
Update executable invocation logic to resolve system binaries (such as `git` and `which`) strictly from the `PATH` environment variable.
This prevents bare command names passed to `execFileSync` / `execFile` from implicitly searching and resolving binaries relative to `process.cwd()` on Windows.
Fixesangular#33755
@alan-agius4
alan-agius4force-pushed the fix-path-executable-resolution branch from e60ed21 to 7d94382CompareAugust 5, 2026 07:03
@bilguunbicktivism

Copy link
Copy Markdown

Thanks for turning this around so fast, and for picking up completion.ts alongside it — that was the sibling call site with the same shape.

One small edge case on findExecutableOnPath, since it is the one thing that could let the original behaviour back in.

join(dir, binaryName + ext) preserves a relativePATH entry, so the returned candidate is not guaranteed to be absolute:

> node -e "const {join,isAbsolute}=require('path'); for (const d of ['.','sub','C:\\Program Files\\Git\\cmd']) { const r=join(d,'git.exe'); console.log(JSON.stringify(d).padEnd(28),'->',JSON.stringify(r),' absolute:',isAbsolute(r)); }"
"." -> "git.exe" absolute: false
"sub" -> "sub\\git.exe" absolute: false
"C:\\Program Files\\Git\\cmd" -> "C:\\Program Files\\Git\\cmd\\git.exe" absolute: true

A . entry collapses to the bare name git.exe. existsSync then resolves it against process.cwd(), and so does execFileSync — which is the original resolution path. The empty-string case is already covered by the if (!rawDir) continue, but . and other relative entries are not.

. on PATH is unusual, but it is the one configuration where the guard would silently no-op rather than fail closed, and the rest of the change is careful to fail closed.

One line covers it:

constdir=rawDir.startsWith('"')&&rawDir.endsWith('"') ? rawDir.slice(1,-1) : rawDir;if(!isAbsolute(dir)){continue;}

or resolve(dir) if relative entries should still be honoured — though skipping them seems closer to the intent of "strictly from system PATH".

Also agree with your framing on the issue itself: a workspace you have chosen to build in already executes project code through lifecycle scripts, builders and schematics, so this is defense in depth rather than a boundary. Thanks for treating it that way.

@alan-agius4

Copy link
Copy Markdown
CollaboratorAuthor

Good call! Updated findExecutableOnPath to skip non-absolute PATH entries so relative paths like . won't fall back to process.cwd() resolution.

@clydin

Copy link
Copy Markdown
Member

Other tools don't do this and it seems quite complex with the mix of PATH/PATHEXT and multiple system calls. Are we sure this is a viable path forward? This can also break legitimate use cases like custom git shims or developer/project specific wrappers.

If we were to do this, would it be better to try setting the NoDefaultCurrentDirectoryInExePath environment variable on Windows early in the CLI's lifecycle to avoid this scenario at the system level? Ref: https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw

@bilguunbicktivism

Copy link
Copy Markdown

@clydin I think you're right, and it's checkable, so I measured it rather than argue. Windows 10.0.26200, Node v24.17.0, a plant git.exe in the working directory that writes a canary and proxies to the real git.

Setting the variable from inside the running process works — it does not need to be inherited:

 case var canaryFired
execFileSync, unset (unset) true <- the bug
execFileSync, "1" "1" false
execFileSync, "" (empty) "" false
execFileSync, "0" "0" false
spawnSync, unset (unset) true
spawnSync, "1" "1" false
shell:true, unset (unset) true
shell:true, "1" "1" false

Negative control, same runs against a directory with no plant: canary never fires and the real git answers, so the false rows are the mitigation and not a broken harness.

Three things that follow, and one of them is a foot-gun:

  1. process.env.NoDefaultCurrentDirectoryInExePath = '1' set early in the CLI lifecycle is sufficient, and it covers more than the PATH resolver does — it also covers shell: true, which goes through cmd.exe and would not be helped by findExecutableOnPath. One line instead of PATH/PATHEXT logic across every call site.

  2. Only definedness matters — the value is ignored."" and even "0" enable it. So it must be deleted rather than set to a falsy value if anyone ever wants it off, and a check like if (process.env.NoDefault... === '1') would be wrong.

  3. It only protects children spawned after the assignment, so "early in the lifecycle" is load-bearing. Anything that spawns during module initialisation, before that line runs, is still on the old behaviour.

On your "custom git shims" concern — that cuts against this approach too, and more bluntly: the env var is process-global, so it disables working-directory resolution for every child the CLI spawns, whereas findExecutableOnPath at least leaves the decision per call site. The trade-off is real either way; the difference is that the env var makes it one visible decision instead of N.

Happy to share the probe scripts if useful.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: reviewThe PR is still awaiting reviews from at least one requested reviewerarea: @angular/cliarea: build & ciRelated the build and CI infrastructure of the projecttarget: patchThis PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants

@alan-agius4@bilguunbicktivism@clydin