CVE-2026-28291
Description
simple-git enables running native Git commands from JavaScript. Versions up to and including 3.31.1 allow execution of arbitrary commands through Git option manipulation, bypassing safety checks meant to block dangerous options like -u and --upload-pack. The flaw stems from an incomplete fix for CVE-2022-25860, as Git's flexible option parsing allows numerous character combinations (e.g., -vu, -4u, -nu) to circumvent the regular-expression-based blocklist in the unsafe operations plugin. Due to the virtually infinite number of valid option variants that Git accepts, a complete blocklist-based mitigation may be infeasible without fully emulating Git's option parsing behavior. This issue has been fixed in version 3.32.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
simple-gitnpm | < 3.32.0 | 3.32.0 |
Affected products
1Patches
11effd8e5012aUnsafe plugin extended to more ways of the `-u` switch being sent to `git.clone`
3 files changed · +98 −15
.changeset/cool-experts-walk.md+8 −0 added@@ -0,0 +1,8 @@ +--- +"simple-git": minor +--- + +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.
simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts+7 −1 modified@@ -3,10 +3,16 @@ import type { SimpleGitPlugin } from './simple-git-plugin'; import { GitPluginError } from '../errors/git-plugin-error'; import type { SimpleGitPluginConfig } from '../types'; +const CLONE_OPTIONS = /^\0*(-|--|--no-)[\0\dlsqvnobucj]+/; + function isConfigSwitch(arg: string | unknown) { return typeof arg === 'string' && arg.trim().toLowerCase() === '-c'; } +function isCloneSwitch(char: string, arg: string | unknown) { + return Boolean(typeof arg === 'string' && CLONE_OPTIONS.test(arg) && arg.includes(char)); +} + function preventProtocolOverride(arg: string, next: string) { if (!isConfigSwitch(arg)) { return; @@ -32,7 +38,7 @@ function preventUploadPack(arg: string, method: string) { ); } - if (method === 'clone' && /^\s*-u\b/.test(arg)) { + if (method === 'clone' && isCloneSwitch('u', arg)) { throw new GitPluginError( undefined, 'unsafe',
simple-git/test/integration/plugin.unsafe.spec.ts+83 −14 modified@@ -1,18 +1,87 @@ -import { promiseError, promiseResult } from '@kwsites/promise-result'; -import { - assertGitError, - createTestContext, - newSimpleGit, - SimpleGitTestContext, -} from '@simple-git/test-utils'; +import { join } from 'node:path'; +import { exists } from '@kwsites/file-exists'; +import { promiseError, PromiseResult, promiseResult } from '@kwsites/promise-result'; +import { assertGitError, createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; import { GitPluginError } from '../..'; -describe('add', () => { +describe('plugin.unsafe', () => { let context: SimpleGitTestContext; + process.env.DEBUG = 'simple-git,simple-git:*'; + beforeEach(async () => (context = await createTestContext())); + describe('CVE-2022-25860: command execution using clone -u', () => { + function pwnedPath() { + return join(context.root, 'pwn', 'touched'); + } + + function isPwned() { + return exists(pwnedPath()); + } + + function expectError(result: PromiseResult<unknown, Error>) { + if (result.success) { + expect(String(result.value).trim().startsWith('usage:')).toBe(true); + } else { + expect(result.error).toBeDefined(); + } + + expect(isPwned()).toBe(false); + } + + beforeEach(async () => { + await context.dir('pwn'); + + const first = newSimpleGit(await context.dir('first')); + await first.init(); + }); + + it('allows local cloning', async () => { + const result = await promiseResult( + newSimpleGit({ baseDir: context.root }) + .clone('./first', './second'), + ); + + expect(result.success).toBe(true); + }); + + it('command injection report', async () => { + for (const i of [45, 54, 52, 45, 118, 115, 113, 110, 108]) { + expectError( + await promiseResult( + newSimpleGit({ baseDir: context.root }) + .clone('./first', './a', [String.fromCharCode(i) + '-u', `sh -c \"touch ${pwnedPath()}\"`]), + ), + ); + + expectError( + await promiseResult( + newSimpleGit({ baseDir: context.root }) + .clone('./first', './b', ['-' + String.fromCharCode(i) + 'u', `sh -c \"touch ${pwnedPath()}\"`]), + ), + ); + + expectError( + await promiseResult( + newSimpleGit({ baseDir: context.root }) + .clone('./first', './c', ['-u' + String.fromCharCode(i), `sh -c \"touch ${pwnedPath()}\"`]), + ), + ); + } + }); + + it('allows clone command injection: `-u...` pattern', async () => { + await promiseResult( + newSimpleGit({ baseDir: context.root, unsafe: { allowUnsafePack: true } }) + .clone('./first', './c', ['-u', `sh -c \"touch ${pwnedPath()}\"`]), + ); + + expect(isPwned()).toBe(true); + }); + }); + it('ignores non string arguments', async () => { const { threw } = await promiseResult(newSimpleGit(context.root).raw([['init']] as any)); @@ -24,8 +93,8 @@ describe('add', () => { newSimpleGit(context.root, { unsafe: { allowUnsafeProtocolOverride: true } }).raw( '-c', 'protocol.ext.allow=always', - 'init' - ) + 'init', + ), ); expect(threw).toBe(false); @@ -35,15 +104,15 @@ describe('add', () => { assertGitError( await promiseError(context.git.raw('-c', 'protocol.ext.allow=always', 'init')), 'Configuring protocol.allow is not permitted', - GitPluginError + GitPluginError, ); }); it('prevents overriding protocol.ext.allow after the method of a command', async () => { assertGitError( await promiseError(context.git.raw('init', '-c', 'protocol.ext.allow=always')), 'Configuring protocol.allow is not permitted', - GitPluginError + GitPluginError, ); }); @@ -53,10 +122,10 @@ describe('add', () => { context.git.clone(`ext::sh -c touch% /tmp/pwn% >&2`, '/tmp/example-new-repo', [ '-c', 'protocol.ext.allow=always', - ]) + ]), ), 'Configuring protocol.allow is not permitted', - GitPluginError + GitPluginError, ); }); });
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/steveukx/git-js/commit/1effd8e5012a5da05a9776512fac3e39b11f2d2dnvdPatchWEB
- github.com/steveukx/git-js/security/advisories/GHSA-jcxm-m3jx-f287nvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-jcxm-m3jx-f287ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28291ghsaADVISORY
- www.cve.org/CVERecordnvdThird Party AdvisoryVDB EntryWEB
- github.com/steveukx/git-js/blob/789c13ebabcf18ebe0b3a0c88ebb4037dede42e3/simple-git/src/lib/plugins/block-unsafe-operations-plugin.tsnvdProductWEB
- github.com/steveukx/git-js/releases/tag/simple-git%403.32.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.