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:
- Has
simple-git ≤ 3.35.xinstalled and the transitive@simple-git/argv-parser ≤ 1.0.3is resolved (lockfiles predating 2026-04-12 are the warning sign), AND - 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.3 | Outcome on simple-git 3.36.0 / argv-parser 1.1.1 | Confidence |
|---|---|---|---|
options[] with --config protocol.ext.allow=always | blocked at argv-parser | blocked at argv-parser | C |
| URL only, hardcoded options/env | blocked by git‘s default protocol.ext.allow=never | blocked by git | C |
.env({GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0}) + ext:: URL | RCE | blocked by parseEnv() in 1.1.0+ | C |
--template=<attacker-writable dir> (clone-time hooks) | secondary RCE if attacker has a separate file-write primitive | blocked by argv-parser’s --template filter | L |
Constructor simpleGit({ config: ['core.pager=…','gpg.program=…',…] }) with attacker-influenced array | Some keys (e.g. core.pager) NOT blocked by 1.0.3; depends on whether the called subcommand triggers them | blocked 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 withext::— git-remote-ext is rare in normal developer workflows; for many estates seeing this in production is itself the alert. - [C]
git_envcarriesGIT_CONFIG_COUNTplus the indexedKEY/VALUEpair settingprotocol.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-typedgit 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.allowUnsafeCustomBinaryfor 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 addallowUnsafeProtocolOverride: truerather 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@'
| Result | Verdict |
|---|---|
no simple-git in tree | Not affected. Stop. |
simple-git ≥ 3.36.0 and argv-parser ≥ 1.1.0 everywhere | Patched. Severity: Informational. Stop. |
simple-git 3.35.x but argv-parser ≥ 1.1.0 resolved | Auto-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.3 | Vulnerable 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.envwritable from outside? Examples: CGI / FastCGI (HTTP headers becomeHTTP_*env vars), container init scripts that copy request headers into env, reverse proxies that pass selected headers as env (ApacheSetEnvIf), 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
| Reach | Vulnerable stack (3.35.2 / 1.0.3) | Patched stack (3.36.0 / 1.1.1) |
|---|---|---|
options[] with --config protocol.ext.allow=always | blocked by argv-parser | blocked by argv-parser |
| URL only, no options | blocked by git (protocol.ext.allow=never) | blocked by git |
.env({GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0}) + ext:: URL | PWNED | blocked 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
gitbinary 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_COUNTin 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
- CVE-2026-6951 — CIRCL Vulnerability-Lookup
- CVE-2026-6951 — OffSeq Threat Radar
- Reporter’s PoC gist (note: the option-array form does not pwn a 3.35.x install)
- Patch commit (steveukx/git-js@89a2294)
- Predecessor — CVE-2022-25912 / GHSA-9p95-fxvg-qgq2
git-remote-ext(1)upstream documentation for theext::protocol macros- MITRE ATT&CK: T1190 (Initial Access — Exploit Public-Facing Application), T1059.004 (Unix Shell), T1543.001 (LaunchDaemon), T1053 (Scheduled Task / Cron), T1082 (System Information Discovery)
- Binautopsy detection rules — github.com/binautopsy/detection-rules
- Reproducer lab — github.com/binautopsy/research-labs/tree/main/cve-2026-6951 (clone, run
npm install, walk the reach map yourself)
Printable PDF
Get this brief as a printable PDF. Drop your work email and we'll send you the link.
We email the link once and don't share your address. Read the privacy notice.