# Leantime: Any Authenticated User Can Self-Escalate to Admin via Unauthorized JSON-RPC Methods

Leantime's JSON-RPC exposes createApiKeyWithProjects without a permission attribute, letting any readonly user create a role=40 admin account and log in as admin.

- Payout: Research
- Severity: high
- Program: Leantime
- Found by: tinyb0y
- Disclosed: 2026-10-15
- CVE: CVE-2026-85990
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-54mm-6729-r5wp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-54mm-6729-r5wp
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:php technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** JSON-RPC API endpoint /api/jsonrpc dispatching @api service methods
**Root cause:** PermissionEnforcer::enforce() silently allows calls when a method lacks a #[RequiresPermission] attribute, and createApiKeyWithProjects (plus four Plugins methods) are @api-exposed without that attribute, forwarding caller-supplied role into addUser() with no clamping.
**Steps:**
1. Authenticate as a readonly-role account; confirm `/plugins/myapps` returns HTTP 403.
2. POST `/api/jsonrpc` with headers `Content-Type: application/json` and `X-Requested-With: XMLHttpRequest` and body `{"jsonrpc":"2.0","id":1,"method":"leantime.rpc.api.api.createApiKeyWithProjects","params":{"values":{"firstname":"poc","role":40,"status":"a"},"projects":[]}}`.
3. Server returns HTTP 200 with `user`, `passwordClean`, and `role:40`.
4. Log in at `/auth/login` with the returned credentials; the session now gets HTTP 200 on `/plugins/myapps`.
**How found:** Code review of the JSON-RPC dispatcher and PermissionEnforcer: noticed enforce() returns silently when #[RequiresPermission] is absent, then checked @api-annotated service methods for missing attributes and compared against the admin-gated web controllers.
**Exploitation:** Attacker with any authenticated session (even readonly) sends one JSON-RPC call carrying their own session cookie plus X-Requested-With, receives generated admin credentials, and logs in through the normal web form for full interactive admin access; the same gap exposes Plugins install/enable/disable/remove.
**Escalation:** Plugin management methods (installPlugin/enablePlugin/disablePlugin/removePlugin) are also unauthenticated-by-role, giving a path toward code execution via arbitrary plugin install.
**Insight:** When authorization is attribute-driven and fails open on absence, enumerate every @api-annotated method and diff it against its web-controller equivalent's role gate; the missing-attribute methods are the bug surface.

