binautopsy · 0x00400420confidentialv24.04 · sha 9f4e…a2c1pgp 0x783E 8C5A
LIVE · incident intake operational pgp 5421 993B … EAB8 0385 lat EU-SW · 42ms tz UTC+01 · AD
uptime 99.98% queue 3 active · 2 pending last note · 2026-04-26
CVE BRIEF

CVE-2026-6951 in simple-git: why the popular PoC doesn’t pwn anything (and the env-var path that does)

CVE-2026-6951

Verdict: conditionally-exploitable

Affected

simple-git ≤ 3.35.x with @simple-git/argv-parser ≤ 1.0.3 in Node.js applications where attacker-influenced data reaches the git env (.env(...) call), or attacker-controlled options[] containing --template, or constructor config arrays with reachable subcommand keys

Verdict: conditionally-exploitable. Vulnerable on simple-git ≤ 3.35.x with @simple-git/argv-parser ≤ 1.0.3 only when the attacker can influence the env passed to git (the .env(...) reach), the --template option, or specific constructor config keys. The popularly-circulated --config protocol.ext.allow=always PoC does NOT work — argv-parser blocks it. Defenders using that PoC to test their stack will reach a false-negative verdict.

Why this CVE

Public summaries describe CVE-2026-6951 as "the previous fix blocked -c but missed --config." Empirically, every shipped argv-parser version (1.0.1, 1.0.2, 1.0.3) catches both forms. The actual gap that 1.1.0 closed is much wider — and the public discussion around the bug is actively misleading defenders into testing wrong.

This brief documents what’s truly exploitable, what isn’t, what each reach looks like to a defender, and the artifacts each post-exploit step leaves on disk and on the wire. Every substantive claim carries a confidence tag: [C] confirmed (we ran it / read the source), [L] likely (consistent reasoning, partial evidence), [S] speculative (plausible, unverified).

Who is affected

Any Node.js application that:

  1. Has simple-git ≤ 3.35.x installed and the transitive @simple-git/argv-parser ≤ 1.0.3 is resolved (lockfiles predating 2026-04-12 are the warning sign), AND
  2. Calls one of the following reach patterns with attacker-influenced input:
Reach (what attacker controls)Outcome on simple-git 3.35.2 / argv-parser 1.0.3Outcome on simple-git 3.36.0 / argv-parser 1.1.1Confidence
options[] with --config protocol.ext.allow=alwaysblocked at argv-parserblocked at argv-parserC
URL only, hardcoded options/envblocked by git‘s default protocol.ext.allow=neverblocked by gitC
.env({GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0}) + ext:: URLRCEblocked by parseEnv() in 1.1.0+C
--template=<attacker-writable dir> (clone-time hooks)secondary RCE if attacker has a separate file-write primitiveblocked by argv-parser’s --template filterL
Constructor simpleGit({ config: ['core.pager=…','gpg.program=…',…] }) with attacker-influenced arraySome keys (e.g. core.pager) NOT blocked by 1.0.3; depends on whether the called subcommand triggers themblocked broadly (1.1.x adds many config-key checks)L

The widest blast surface is CI runners, GitOps controllers, code-review bots, and deploy hooks — anywhere simple-git appears downstream of a request-derived env, header-to-env mapping (FastCGI, Lambda runtime layers), or job-config-derived options array.

Exploitability assessment

[C] Verified attack chain. On a host with simple-git@3.35.2 and @simple-git/argv-parser@1.0.3, calling simpleGit().env({…GIT_CONFIG_*…}).clone('ext::sh -c …', dest) yields arbitrary command execution as the host’s Node process user. The side-effect runs even though the clone itself fails — git aborts when the ext-helper does not speak git’s pack protocol, but the helper has already forked a shell.

Spawn fingerprint as captured by our trace wrapper:

{
  "event": "git-spawn",
  "argv": ["clone","--","ext::sh -c <attacker-payload>","<dest>"],
  "git_env": {
    "GIT_CONFIG_COUNT": "1",
    "GIT_CONFIG_KEY_0": "protocol.ext.allow",
    "GIT_CONFIG_VALUE_0": "always"
  }
}

Three things in this fingerprint matter to a detector:

  • [C] argv[2] starts with ext:: — git-remote-ext is rare in normal developer workflows; for many estates seeing this in production is itself the alert.
  • [C] git_env carries GIT_CONFIG_COUNT plus the indexed KEY/VALUE pair setting protocol.ext.allow=always. argv-parser 1.0.x has no env scanner; this is the bypass.
  • [C] argv[1] is --, immediately followed by the malicious URL — simple-git always emits the -- separator before the source argument, which is a reliable structural signature distinguishing this from a hand-typed git clone.

[C] The myth in the public PoC. The reporter’s gist and most CVE summaries describe the bypass as "the previous fix blocked -c but missed --config." Empirically, every shipped @simple-git/argv-parser version (1.0.1, 1.0.2, 1.0.3) catches both forms via the same regex. The naive options-array PoC

simpleGit().clone(url, dest, ['--config','protocol.ext.allow=always'])

throws Configuring protocol.allow is not permitted without enabling allowUnsafeProtocolOverride on simple-git 3.35.2 — before git is ever spawned. A defender who finds source code matching that exact shape should not assume RCE. The reachability question is whether the env or a --template path is influenceable, not whether --config is present in the options array.

[C] Process tree captured during exploitation:

node host-app.js                              # the host application
└── git clone -- ext::… <dest>                # spawned by simple-git
    └── git-remote-ext "ext::…"               # transport helper
        └── sh -c "<attacker-payload>"        # the RCE shell
            └── (whatever the attacker chose) # touch / printf / bash -i / …

[C] The string ext:: appears in the argv of both the git clone and the git-remote-ext processes — two-deep redundancy makes argv-truncation evasion harder. The sh -c immediate child is unusual for legitimate git workflows; only the ext-helper produces it. [L] On Linux, /proc/<pid>/environ of the git process retains GIT_CONFIG_COUNT and the indexed key/value at attack time — EDRs that snapshot environment on process_create (Sysmon-for-Linux ProcessCreate with IncludeEnvironmentVariables, recent auditd with --with-env) will surface the indicator without needing a tracer.

[L] Risky combinations.

  • Any CVE that lets an unauthenticated user populate request-derived env vars (header→env mappings in some app servers, FastCGI, AWS Lambda runtime layers passing client headers) chains directly into this CVE if the app uses simple-git anywhere downstream.
  • Self-hosted CI runners that use simple-git (or wrapper SDKs built on it) in a job that takes a "clone URL" or "target ref" parameter from a webhook payload. The parameter is rarely env, but options arrays built from job config are common.
  • [S] Applications that already opted in to unsafe.allowUnsafeCustomBinary for legitimate reasons may have implicitly normalised users’ tolerance for argv-parser warnings, making it more likely that a developer responding to "Configuring protocol.allow is not permitted" would just add allowUnsafeProtocolOverride: true rather than investigate. Worth flagging in code review.

Fingerprint your exposure

Walk these steps in your own environment. The empirical answer to "are we exposed?" comes from the dependency-tree check (Step 1) plus the call-site reach map (Step 3).

Step 1 — Is the dependency stack actually vulnerable?

Run inside the audited project:

npm ls simple-git @simple-git/argv-parser --all 2>&1 | grep -E 'simple-git@|argv-parser@'
ResultVerdict
no simple-git in treeNot affected. Stop.
simple-git ≥ 3.36.0 and argv-parser ≥ 1.1.0 everywherePatched. Severity: Informational. Stop.
simple-git 3.35.x but argv-parser ≥ 1.1.0 resolvedAuto-mitigated by transitive resolution after 2026-04-12. Recommend pinning simple-git ≥ 3.36.0 for clarity. Severity: Low.
simple-git ≤ 3.35.x and argv-parser ≤ 1.0.3Vulnerable stack present. Continue to Step 2.

If the project uses Yarn / pnpm / a private registry mirror, also check whether the lockfile predates 2026-04-12 (when argv-parser 1.1.0 was published). A lockfile from before that date is the warning sign.

Step 2 — Find every call site

grep -RIn --include='*.{js,ts,mjs,cjs,jsx,tsx}' \
  -E "require\(['\"]simple-git['\"]\)|from ['\"]simple-git['\"]" .

For each match, identify the simple-git instance and trace which methods are invoked: .clone(), .raw(), .pull(), .fetch(), .push(), .env(), and the constructor simpleGit({config: [...]}).

Step 3 — Reach mapping per call site

For each call site, answer the three reach questions in order. Stop at the first YES.

A. Does the attacker influence the env passed to simple-git? (the real bypass)

  • Does any code path call simpleGit().env(<env>) where <env> is built from request data, query params, headers, form fields, deserialised JSON, or any field the attacker can populate?
  • Is the host process’s own process.env writable from outside? Examples: CGI / FastCGI (HTTP headers become HTTP_* env vars), container init scripts that copy request headers into env, reverse proxies that pass selected headers as env (Apache SetEnvIf), job queues / serverless platforms allowing per-invocation env.
  • Does any sibling process (parent shell, supervisor, sidecar) merge attacker input into env before spawning the simple-git host?

If YES → reproduce against the audited app’s exact env shape. Severity: Critical.

B. Does the attacker influence the options array passed to a simple-git method?

  • Is the third argument of clone(url, dest, options) derived from untrusted input?
  • Same for raw(commands), pull(remote, branch, options), etc.
  • Is the constructor option simpleGit({config: [...]}) derived from untrusted input?

If YES and attacker can supply --template <attacker-controlled-dir> and they have a separate file-write primitive on the host: secondary RCE via clone-time hooks (hooks/post-checkout). Severity: depends on whether the file-write primitive exists. Often High.

If YES but only the --config protocol.*allow=always shape is reachable: the public PoC framing is blocked by argv-parser even at 1.0.3. Severity: Informational unless combined with route A or C. Document the call site for hardening but don’t claim RCE.

C. Does the attacker influence the URL only, with options/env hardcoded?

Even passing ext::sh -c … as the URL is rejected by git (default protocol.ext.allow=never). Severity: Informational. Note as a defence-in-depth finding (you depend on git’s protocol-allow default) but not exploitable as CVE-2026-6951.

Decision tree summary

ReachVulnerable stack (3.35.2 / 1.0.3)Patched stack (3.36.0 / 1.1.1)
options[] with --config protocol.ext.allow=alwaysblocked by argv-parserblocked by argv-parser
URL only, no optionsblocked by git (protocol.ext.allow=never)blocked by git
.env({GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0}) + ext:: URLPWNEDblocked by argv-parser’s parseEnv
--template=<attacker-writable dir with hooks>secondary RCE (depends on file-write primitive)blocked by argv-parser’s --template filter
Constructor {config: ['protocol.ext.allow=always']}blocked (prefixed as -c, then argv-parser catches it)blocked

Mitigations

npm install simple-git@^3.36.0 (which transitively pulls argv-parser@^1.1.0) closes the bug. For environments where the upgrade is delayed, layer:

  • [C] Strip GIT_CONFIG_* from any env passed to simple-git — single call: Object.keys(env).filter(k => !k.startsWith('GIT_CONFIG_')).forEach(k => delete env[k]). This removes the env-var injection vector regardless of argv-parser version.
  • [C] Pass unsafe: { allowUnsafeProtocolOverride: false, … } (or rely on the default) and never opt in.
  • [C] Configure the git binary itself to refuse the ext protocol globally: git config --system protocol.ext.allow never. This is defence-in-depth even on patched stacks.
  • [L] Run the simple-git host process with a sanitised env (e.g. via systemd EnvironmentFile= + Environment= overrides, or a small spawn shim that whitelists env keys).
  • [L] Apply WAF/middleware rules to any HTTP endpoint that takes a JSON body and surfaces it to git. A single POST containing GIT_CONFIG_COUNT in the body is a strong signal — block by request-body inspection rather than path.

For SOC teams: deploy the two Sigma rules at github.com/binautopsy/detection-rules/sigma/cve-2026-6951-env-injection.yml and …/cve-2026-6951-post-exploit.yml. The first fires on the canonical spawn pattern; the second catches descendants of git-remote-ext-driven shells doing classic post-exploit actions (persistence-path writes, /dev/tcp/ opens, network-fetch utilities).

References

Printable PDF

Get this brief as a printable PDF. Drop your work email and we'll send you the link.

    Send me Binautopsy's monthly research digest

    We email the link once and don't share your address. Read the privacy notice.