Command Injection
Description
The package czproject/git-php before 4.0.3 are vulnerable to Command Injection via git argument injection. When calling the isRemoteUrlReadable($url, array $refs = NULL) function, both the url and refs parameters are passed to the git ls-remote subcommand in a way that additional flags can be set. The additional flags can be used to perform a command injection.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A command injection vulnerability in czproject/git-php before 4.0.3 allows attackers to execute arbitrary commands via specially crafted URL or refs arguments to isRemoteUrlReadable().
Vulnerability
The czproject/git-php library prior to version 4.0.3 contains a command injection vulnerability in the isRemoteUrlReadable($url, array $refs = NULL) function. Both the $url and $refs parameters are passed unsanitized to the git ls-remote subcommand, allowing an attacker to inject additional git flags and commands. The fix was introduced in version 4.0.3 by using --end-of-options for similar functions across the codebase [1][3][4].
Exploitation
An attacker with the ability to control the $url or $refs arguments passed to isRemoteUrlReadable() (e.g., via user-supplied remote URL or ref list in an application using the library) can inject arbitrary git flags. By crafting input such as --upload-pack=... or -c key=value, the attacker can execute arbitrary shell commands on the server running the vulnerable code. No authentication is required beyond the ability to invoke the vulnerable method [1][2].
Impact
Successful exploitation allows an attacker to achieve arbitrary command execution with the privileges of the PHP process. This can lead to full compromise of the application server, including data exfiltration, installation of backdoors, or lateral movement within the network. The vulnerability is classified as critical due to the potential for remote code execution [1].
Mitigation
Upgrade to czproject/git-php version 4.0.3 or later, which implements --end-of-options to separate options from arguments and prevent injection in all affected commands [3][4]. If immediate upgrade is not possible, ensure that no user-supplied input reaches the isRemoteUrlReadable() function without strict validation or escaping. As of the publication date (2022-04-25), no known workarounds other than patching are documented [1].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
czproject/git-phpPackagist | < 4.0.3 | 4.0.3 |
Affected products
3- czproject/git-phpdescription
Patches
15e82d5479da5Uses --end-of-options after command options (for security reasons)
6 files changed · +48 −45
src/Git.php+3 −0 modified@@ -46,6 +46,7 @@ public function init($directory, array $params = NULL) $this->run($directory, [ 'init', $params, + '--end-of-options', $directory ]); @@ -89,6 +90,7 @@ public function cloneRepository($url, $directory = NULL, array $params = NULL) $this->run($cwd, [ 'clone', $params, + '--end-of-options', $url, $directory ]); @@ -120,6 +122,7 @@ public function isRemoteUrlReadable($url, array $refs = NULL) '--heads', '--quiet', '--exit-code', + '--end-of-options', $url, $refs, ], [
src/GitRepository.php+15 −15 modified@@ -52,7 +52,7 @@ public function getRepositoryPath() */ public function createTag($name, $options = NULL) { - $this->run('tag', $options, $name); + $this->run('tag', $options, '--end-of-options', $name); return $this; } @@ -86,7 +86,7 @@ public function renameTag($oldName, $newName) { // http://stackoverflow.com/a/1873932 // create new as alias to old (`git tag NEW OLD`) - $this->run('tag', $newName, $oldName); + $this->run('tag', '--end-of-options', $newName, $oldName); // delete old (`git tag -d OLD`) $this->removeTag($oldName); return $this; @@ -114,7 +114,7 @@ public function getTags() */ public function merge($branch, $options = NULL) { - $this->run('merge', $options, $branch); + $this->run('merge', $options, '--end-of-options', $branch); return $this; } @@ -131,7 +131,7 @@ public function merge($branch, $options = NULL) public function createBranch($name, $checkout = FALSE) { // git branch $name - $this->run('branch', $name); + $this->run('branch', '--end-of-options', $name); if ($checkout) { $this->checkout($name); @@ -234,7 +234,7 @@ public function getLocalBranches() */ public function checkout($name) { - $this->run('checkout', $name); + $this->run('checkout', '--end-of-options', $name); return $this; } @@ -253,7 +253,7 @@ public function removeFile($file) } foreach ($file as $item) { - $this->run('rm', $item, '-r'); + $this->run('rm', '-r', '--end-of-options', $item); } return $this; @@ -282,7 +282,7 @@ public function addFile($file) throw new GitException("The path at '$item' does not represent a valid file."); } - $this->run('add', $item); + $this->run('add', '--end-of-options', $item); } return $this; @@ -319,7 +319,7 @@ public function renameFile($file, $to = NULL) } foreach ($file as $from => $to) { - $this->run('mv', $from, $to); + $this->run('mv', '--end-of-options', $from, $to); } return $this; @@ -454,7 +454,7 @@ public function hasChanges() */ public function pull($remote = NULL, array $params = NULL) { - $this->run('pull', $remote, $params); + $this->run('pull', $params, '--end-of-options', $remote); return $this; } @@ -468,7 +468,7 @@ public function pull($remote = NULL, array $params = NULL) */ public function push($remote = NULL, array $params = NULL) { - $this->run('push', $remote, $params); + $this->run('push', $params, '--end-of-options', $remote); return $this; } @@ -482,7 +482,7 @@ public function push($remote = NULL, array $params = NULL) */ public function fetch($remote = NULL, array $params = NULL) { - $this->run('fetch', $remote, $params); + $this->run('fetch', $params, '--end-of-options', $remote); return $this; } @@ -497,7 +497,7 @@ public function fetch($remote = NULL, array $params = NULL) */ public function addRemote($name, $url, array $params = NULL) { - $this->run('remote', 'add', $params, $name, $url); + $this->run('remote', 'add', $params, '--end-of-options', $name, $url); return $this; } @@ -511,7 +511,7 @@ public function addRemote($name, $url, array $params = NULL) */ public function renameRemote($oldName, $newName) { - $this->run('remote', 'rename', $oldName, $newName); + $this->run('remote', 'rename', '--end-of-options', $oldName, $newName); return $this; } @@ -524,7 +524,7 @@ public function renameRemote($oldName, $newName) */ public function removeRemote($name) { - $this->run('remote', 'remove', $name); + $this->run('remote', 'remove', '--end-of-options', $name); return $this; } @@ -539,7 +539,7 @@ public function removeRemote($name) */ public function setRemoteUrl($name, $url, array $params = NULL) { - $this->run('remote', 'set-url', $params, $name, $url); + $this->run('remote', 'set-url', $params, '--end-of-options', $name, $url); return $this; }
tests/GitPhp/GitRepository.branches.phpt+5 −5 modified@@ -10,12 +10,12 @@ require __DIR__ . '/bootstrap.php'; $runner = new AssertRunner(__DIR__); $git = new Git($runner); -$runner->assert(['branch', 'master']); -$runner->assert(['branch', 'develop']); -$runner->assert(['checkout', 'develop']); -$runner->assert(['merge', 'feature-1']); +$runner->assert(['branch', '--end-of-options', 'master']); +$runner->assert(['branch', '--end-of-options', 'develop']); +$runner->assert(['checkout', '--end-of-options', 'develop']); +$runner->assert(['merge', '--end-of-options', 'feature-1']); $runner->assert(['branch', '-d', 'feature-1']); -$runner->assert(['checkout', 'master']); +$runner->assert(['checkout', '--end-of-options', 'master']); $repo = $git->open(__DIR__); $repo->createBranch('master');
tests/GitPhp/GitRepository.files.phpt+13 −13 modified@@ -14,11 +14,11 @@ $repo = $git->open(__DIR__ . '/fixtures'); test(function () use ($repo, $runner) { $runner->resetAsserts(); - $runner->assert(['add', 'file1.txt']); - $runner->assert(['add', 'file2.txt']); - $runner->assert(['add', 'file3.txt']); - $runner->assert(['add', 'file4.txt']); - $runner->assert(['add', 'file5.txt']); + $runner->assert(['add', '--end-of-options', 'file1.txt']); + $runner->assert(['add', '--end-of-options', 'file2.txt']); + $runner->assert(['add', '--end-of-options', 'file3.txt']); + $runner->assert(['add', '--end-of-options', 'file4.txt']); + $runner->assert(['add', '--end-of-options', 'file5.txt']); $repo->addFile('file1.txt'); $repo->addFile([ @@ -38,11 +38,11 @@ test(function () use ($repo) { test(function () use ($repo, $runner) { $runner->resetAsserts(); - $runner->assert(['rm', 'file1.txt', '-r']); - $runner->assert(['rm', 'file2.txt', '-r']); - $runner->assert(['rm', 'file3.txt', '-r']); - $runner->assert(['rm', 'file4.txt', '-r']); - $runner->assert(['rm', 'file5.txt', '-r']); + $runner->assert(['rm', '-r', '--end-of-options', 'file1.txt']); + $runner->assert(['rm', '-r', '--end-of-options', 'file2.txt']); + $runner->assert(['rm', '-r', '--end-of-options', 'file3.txt']); + $runner->assert(['rm', '-r', '--end-of-options', 'file4.txt']); + $runner->assert(['rm', '-r', '--end-of-options', 'file5.txt']); $repo->removeFile('file1.txt'); $repo->removeFile([ @@ -55,9 +55,9 @@ test(function () use ($repo, $runner) { test(function () use ($repo, $runner) { $runner->resetAsserts(); - $runner->assert(['mv', 'file1.txt', 'new1.txt']); - $runner->assert(['mv', 'file2.txt', 'new2.txt']); - $runner->assert(['mv', 'file3.txt', 'new3.txt']); + $runner->assert(['mv', '--end-of-options', 'file1.txt', 'new1.txt']); + $runner->assert(['mv', '--end-of-options', 'file2.txt', 'new2.txt']); + $runner->assert(['mv', '--end-of-options', 'file3.txt', 'new3.txt']); $repo->renameFile('file1.txt', 'new1.txt'); $repo->renameFile([
tests/GitPhp/GitRepository.remotes.phpt+10 −10 modified@@ -10,17 +10,17 @@ require __DIR__ . '/bootstrap.php'; $runner = new AssertRunner(__DIR__); $git = new Git($runner); -$runner->assert(['clone', '-q', 'git@github.com:czproject/git-php.git', __DIR__]); -$runner->assert(['remote', 'add', 'origin2', 'git@github.com:czproject/git-php.git']); -$runner->assert(['remote', 'add', 'remote', 'git@github.com:czproject/git-php.git']); +$runner->assert(['clone', '-q', '--end-of-options', 'git@github.com:czproject/git-php.git', __DIR__]); +$runner->assert(['remote', 'add', '--end-of-options', 'origin2', 'git@github.com:czproject/git-php.git']); +$runner->assert(['remote', 'add', '--end-of-options', 'remote', 'git@github.com:czproject/git-php.git']); $runner->assert(['remote', 'add', [ '--mirror=push', -], 'only-push', 'test-url']); -$runner->assert(['remote', 'rename', 'remote', 'origin3']); +], '--end-of-options', 'only-push', 'test-url']); +$runner->assert(['remote', 'rename', '--end-of-options', 'remote', 'origin3']); $runner->assert(['remote', 'set-url', [ '--push', -], 'origin3', 'test-url']); -$runner->assert(['remote', 'remove', 'origin2']); +], '--end-of-options', 'origin3', 'test-url']); +$runner->assert(['remote', 'remove', '--end-of-options', 'origin2']); $repo = $git->cloneRepository('git@github.com:czproject/git-php.git', __DIR__); $repo->addRemote('origin2', 'git@github.com:czproject/git-php.git'); @@ -34,9 +34,9 @@ $repo->setRemoteUrl('origin3', 'test-url', [ ]); $repo->removeRemote('origin2'); -$runner->assert(['push', 'origin']); -$runner->assert(['fetch', 'origin']); -$runner->assert(['pull', 'origin']); +$runner->assert(['push', '--end-of-options', 'origin']); +$runner->assert(['fetch', '--end-of-options', 'origin']); +$runner->assert(['pull', '--end-of-options', 'origin']); $repo->push('origin'); $repo->fetch('origin'); $repo->pull('origin');
tests/GitPhp/GitRepository.tags.phpt+2 −2 modified@@ -10,8 +10,8 @@ require __DIR__ . '/bootstrap.php'; $runner = new AssertRunner(__DIR__); $git = new Git($runner); -$runner->assert(['tag', 'v1.0.0']); -$runner->assert(['tag', 'v2.0.0', 'v1.0.0']); +$runner->assert(['tag', '--end-of-options', 'v1.0.0']); +$runner->assert(['tag', '--end-of-options', 'v2.0.0', 'v1.0.0']); $runner->assert(['tag', '-d', 'v1.0.0']); $runner->assert(['tag', '-d', 'v2.0.0']);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-3xpw-vhmv-cw7hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-25866ghsaADVISORY
- github.com/czproject/git-php/commit/5e82d5479da5f16d37a915de4ec55e1ac78de733ghsax_refsource_MISCWEB
- github.com/czproject/git-php/releases/tag/v4.0.3ghsax_refsource_MISCWEB
- snyk.io/vuln/SNYK-PHP-CZPROJECTGITPHP-2421349ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.