Skip to content

Measured transcript for the CWD executable injection fixed in #3905, and a question about publishing an advisory #3910

Description

@bilguunbicktivism

Google's OSS VRP closed my report on this (issue 542173050) and its closing comment says:

"Please feel free to publicly disclose this issue on GitHub as a public issue."

So this is that. It is filed to put the measurement on the public record next to the fix, not to
re-argue severity — I accept the assessment. There is one question at the end.

What was measured

ng-dev pr rebase <n> checks the pull request out into the maintainer's working tree and then runs
git as a bare command name. On Windows, CreateProcess resolves a bare name from the current
directory before PATH, so the git.exe that runs is the one the pull request just placed there.

The run below is from a clean clone of my own repository. The maintainer populates nothing; the
only thing on disk that did not come from git clone is the single file the contributor's pull
request adds.

=== STEP 2 — NEGATIVE CONTROL: the tree is clean before anything runs ===
  executables anywhere in the working tree : NONE
  where.exe git, resolved from inside this repository:
    C:\Program Files\Git\cmd\git.exe
  CANARY.txt present : False

=== STEP 3 — what the outside contributor's pull request contains ===
  PR #1, files changed:
    git.exe
  diffstat vs main:
     git.exe | Bin 0 -> 6656 bytes
     1 file changed, 0 insertions(+), 0 deletions(-)

=== STEP 4 — ONE command. The documented maintainer workflow. ===
  $ ng-dev pr rebase 1
    Checking out PR #1 from …
    Rebase was able to complete automatically without conflicts
    Rebased and updated PR #1

The planted binary ran 11 times during that one command. Two of those calls carried the token as
a command-line argument, and every call inherited it in the environment:

    PLANT ran as ArrA | cwd=…\maintainer-clone
            argv    = [-c credential.helper= fetch -q https://x-access-token:<TOKEN len=40>@github.com/… --deepen=500 -- main]
            token in argv        : YES
            GITHUB_TOKEN inherited: <PRESENT len=40>
    …
            argv    = [-c credential.helper= push https://x-access-token:<TOKEN len=40>@github.com/… HEAD:contributor-adds-one-file --force-with-lease=…]
            token in argv        : YES
            GITHUB_TOKEN inherited: <PRESENT len=40>

Two details that were not obvious to me until I ran it:

  • The tool's own cleanup does not remove the file. It checks the branch back out at the end, so
    git.exe is no longer tracked — but Windows will not delete a running image, so the binary stays
    on disk as an untracked file:

    === STEP 6 ===
      git.exe is tracked at this HEAD : NO - the checkout reverted it
      git.exe STILL ON DISK          : yes, 6656 bytes
      what git thinks of it now      : ?? git.exe
    
  • It then runs again on unrelated commands. A later ng-dev release info, which has nothing to do
    with pull requests, executed it again:

    === STEP 7 ===
      canary lines before : 44
      $ ng-dev release info
      canary lines after  : 60
    

Tool under test: @angular/ng-dev@0.0.0-c279805293db7c28fe20709bc7ac3a3f3bbebb40, driven through its
own node_modules/.bin/ng-dev.cmd shim, with NoDefaultCurrentDirectoryInExePath unset at process,
user and machine scope — i.e. a stock Windows box.

The fix already landed, and I checked that it is complete

#3905"fix(ng-dev): prevent current working directory executable injection on Windows",
92c6b596e59e320c9edbc6d6490c1047264c4a0d — sets NoDefaultCurrentDirectoryInExePath in
ng-dev/cli.ts, and that covers this path too, not only the Prompt.editor case in #3897.

I did not assume that. I tried to find a spawn shape the fix misses, because the variable's semantics
are definedness-only and I expected cmd.exe to do its own search:

=== A. the path the fix was written for: CreateProcess, no shell ===
  spawnSync bare name, shell:false   mitigation=off  plantRan=TRUE
  spawnSync bare name, shell:false   mitigation=ON   plantRan=FALSE
=== B. the shell family -- cmd.exe does its own search ===
  execSync("git --version")          mitigation=ON   plantRan=FALSE
  spawnSync bare name, shell:true    mitigation=ON   plantRan=FALSE
  execFileSync bare, shell:true      mitigation=ON   plantRan=FALSE
=== C. grandchild spawn ===         mitigation=ON   plantRan=FALSE
  rows that still run WITH the fix : 0

cmd.exe honours the variable and grandchildren inherit it, so the one-line fix holds across the
shell family as well. My hypothesis that the fix was incomplete was wrong, and I would rather say
so here than leave it hanging.

The question

The fix shipped as an ordinary fix(...) commit with no advisory. Anyone still on an ng-dev build
from before 2026-08-06 has the behaviour and no signal that it changed.

Would you consider publishing a GitHub security advisory for it? I am not arguing about severity —
the maintainer assessment on the sibling issue was that this does not cross a security boundary, and
that is your call, not mine. The reason to ask is narrower: an advisory is what makes an already-merged
fix discoverable to people running older builds, and the commit's own title calls the behaviour
"executable injection".

If the answer is no, that is a completely reasonable answer and this issue can just be closed — the
transcript will still be here for anyone who searches for it later.

Happy to attach the full scripts and raw logs, or to re-run anything on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions