CVE-2026-6951 - Bypassing simple-git's Blocklist via the --config Flag (RCE Analysis)
DEV Community

CVE-2026-6951 - Bypassing simple-git's Blocklist via the --config Flag (RCE Analysis)

| Field | Value | |---|---| | CVE ID | CVE-2026-6951 | | Affected package | simple-git (npm) | | Affected versions | &2" /tmp/dest This is disabled by default and only works once the protocol.ext.allow=always config value is turned on - a value that can be injected at the command line with -c or --config . git clone -c protocol.ext.allow=always "ext::sh -c '...'" /tmp/dest So the attack needs two conditions at once: - The ability to inject protocol.ext.allow=always - The ability to inject a malicious clone URL starting with ext:: simple-git passes the options (or customArgs ) array that callers provide to clone() , fetch() , etc. almost verbatim to the git binary. If an application mixes untrusted input (user input, external API responses) into that array, an attacker can satisfy both conditions above. The 2022 patch addressed condition 1 by detecting and blocking arguments like -c protocol...allow=... with a blocklist regex. 3. The 2022 Patch's Structure - and Its Limit The patch lives in simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts , with preventProtocolOverride as the core function. Simplified: // block-unsafe-operations-plugin.ts (2022 patch, conceptually reconstructed) function preventProtocolOverride(args: string[]) { for (let i = 0; i &2', // โ‘  malicious clone URL '/tmp/example-new-repo', // โ‘ก clone destination directory ['--config', 'protocol.ext.allow=always'] // โ‘ข the bypass option ); Breaking it down: - โ‘  The first argument to clone() is normally where a real repository URL goes. Passingext::sh -c touch% /tmp/pwned% >&2 tells git: "clone via theext protocol, and runsh -c touch /tmp/pwned as the helper command." (% is how git encodes spaces inside this kind of URL.) - โ‘ก The second argument is just the normal clone destination path - irrelevant to the attack itself. - โ‘ข The third argument is the crux. --config andprotocol.ext.allow=always are passed as two separate array elements. This has the exact same effect as-c protocol.ext.allow=always from git's perspective - butsimple-git 's blocklist only looks for the literal string'-c' , so the'--config' token sails right past it. Under the hood, simple-git ends up executing exactly this: git clone --config protocol.ext.allow=always "ext::sh -c touch% /tmp/pwned% >&2" /tmp/example-new-repo Once this command runs, the ext:: helper process executes before git reports a clone failure (since the "repository" doesn't actually exist), creating /tmp/pwned . In other words, even if the final clone fails, the command execution has already succeeded - an application that catches the clone failure gracefully has no indication that it was already compromised. Mapped onto a realistic application scenario, the danger becomes clearer: // The application's intended, safe-looking call simpleGit().clone( 'https://github.com/my-org/my-repo', '/tmp/workdir', userSuppliedOptions // an options array under user control ); // But if an attacker can inject these two values into userSuppliedOptions... userSuppliedOptions = ['--config', 'protocol.ext.allow=always']; // ...and also control the clone URL itself, it's a direct path to RCE 6. Patch Analysis (simple-git 3.36.0) The fix isn't just "add --config to the blocklist" - it's a meaningfully larger architectural change, touching 29 files and 1,000+ lines under the commit name "Environment Parsing." 6-1. Expanded config-key blocklist The old detect-config-writes.ts blocked only six keys: protocol.allow , core.sshCommand , core.fsmonitor , core.gitProxy , core.hooksPath , diff.external . The new detect-vulnerable-config-writes.ts introduces a preventExpandedConfigBuilder helper with an expanded regex that also catches scoped variants like credential.https://example.com.helper , and adds blocking for: - alias.* - registering arbitrary commands as git aliases - core.askPass ,core.editor ,core.pager - binary substitution - credential.helper - credential interception - diff.textconv ,filter.clean ,filter.smudge - content-filter chains that can intercept file contents - the gpg.program family - signing-binary substitution - merge.driver ,mergetool.cmd - code execution at merge time - sequence.editor - binary substitution during rebase 6-2. Expanded flag detection The old detect-upload-pack.ts , which only caught --upload-pack /--receive-pack -style flags, is replaced by detect-vulnerable-flags.ts , which now also blocks --template (hook-planting via a custom template directory). 6-3. Environment variable scanning - an entirely new line of defense This is the most important change in the patch. Git accepts the same kind of config injection not just via -c /--config on the command line, but also via environment variables like GIT_CONFIG_COUNT , GIT_CONFIG_KEY_n , and GIT_CONFIG_VALUE_n . That means filtering command-line arguments alone could never be a complete fix. The new parseEnv() function maps dangerous environment variables (GIT_ASKPASS , GIT_SSH_COMMAND , GIT_CONFIG_COUNT , GIT_EXTERNAL_DIFF , GIT_PROXY_COMMAND , and more), and when GIT_CONFIG_COUNT is set, extracts the corresponding GIT_CONFIG_KEY_n /VALUE_n pairs and runs them through the same blocklist check. // Before the patch: only arguments are checked action(args) { const parsed = parseArgv(...args); for (const v of parsed.vulnerabilities.vulnerabilities) { /* throw / } } // After the patch: arguments AND environment variables are checked action(args, { env }) { for (const v of vulnerabilityCheck(args, env)) { / throw */ } } In plain terms: the gatekeeper used to only look at "what came in on the command line." Now it also checks "what's been smuggled in through environment variables to achieve the same config injection." Even if an attacker's command-line arguments slip past validation, the same attack attempted through environment variables now gets caught by vulnerabilityCheck() . 6-4. A more granular opt-in model The patch introduces per-category opt-in flags - allowUnsafeAlias , allowUnsafeCredentialHelper , allowUnsafeTemplateDir , and more - so developers who genuinely need one of these features can explicitly enable it for trusted input only. VulnerabilityCategoryFlags , which previously had 7 entries, now has more than 20. 7. Why This Keeps Happening - The Structural Cause This is the third related vulnerability in simple-git : - CVE-2022-25912: the original RCE via the -c flag - CVE-2026-28292: bypassing the case-sensitive regex with uppercase PROTOCOL.ALLOW=always - CVE-2026-6951: bypassing the blocklist entirely via the --config synonym All three share the same root cause. A blocklist that matches one exact dangerous pattern will always be bypassable the moment a different spelling with the same meaning exists - short vs. long form, case variants, environment-variable paths, and so on. The fact that the 3.36.0 patch widens the defense to cover environment variables as well as command-line arguments is effectively an admission that single-point blocklists have a hard ceiling, and a move toward defense in depth instead. 8. Remediation | Action | Details | |---|---| | Upgrade | npm install simple-git@latest (3.36.0 or later) | | Watch for partial upgrades | Mismatched sub-package versions (e.g. @simple-git/argv-parser ) can cause TypeError s in CI - regenerating yarn.lock /package-lock.json entirely is recommended | | Validate input | Never pass untrusted input directly into options /customArgs - map it through an allowlist of accepted values instead | | Least privilege | Run the process that invokes simple-git under a sandboxed or restricted-privilege account | Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.