{"id":"CVE-2026-32638","aliases":["EUVD-2026-12964","GHSA-xvf4-ch4q-2m24"],"status":"published","title":"StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens","description":"StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.4, 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. Version 0.4.4 fixes the issue.","severity":{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00398,"percentile":0.3436,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-639"],"published":"2026-03-16T16:37:42Z","modified":"2026-10-28T02:30:28Z","fixes":[{"ecosystem":"npm","package":"studiocms","fixed_in":["0.4.4"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"studiocms","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.4.3"},{"fixed":"0.4.4"}]}]},{"source":"osv","ecosystem":"npm","package":"studiocms","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.4.4"}]}]}],"products":[{"vendor":"studiocms","product":"studiocms"},{"vendor":"withstudiocms","product":"studiocms"}],"references":[{"url":"https://github.com/withstudiocms/studiocms/security/advisories/GHSA-xvf4-ch4q-2m24","tags":["x_refsource_CONFIRM","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/withstudiocms/studiocms/commit/aebe8bcb3618bb07c6753e3f5c982c1fe6adea64","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/withstudiocms/studiocms/releases/tag/studiocms@0.4.4","tags":["x_refsource_MISC","Product","Release Notes","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-32638","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-xvf4-ch4q-2m24"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32638.json","tags":["advisory"]},{"url":"https://github.com/withstudiocms/studiocms","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-32638","title":"StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens","description":"StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.4, 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. Version 0.4.4 fixes the issue.","status":"published","published":"2026-03-18T20:41:14.034Z","modified":"2026-03-19T15:01:00.396Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low","assessor":"cna"}],"cwes":["CWE-639"],"updated_at":"2026-11-02T14:19:57.100857Z"}],"euvd":[{"id":"EUVD-2026-12964","description":"StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.4, 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. Version 0.4.4 fixes the issue.","status":"published","published":"2026-03-18T20:41:14Z","modified":"2026-03-19T15:01:00Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low"}],"updated_at":"2026-11-02T19:25:32.813864Z"}],"ghsa":[{"id":"GHSA-xvf4-ch4q-2m24","title":"StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens","description":"## Summary\n\nThe 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.\n\n## Details\n\n### Vulnerable Code Path\n\nFile: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1605-1647\n\n```ts\n.handle(\n    'getUsers',\n    Effect.fn(\n        function* ({ urlParams: { name, rank, username } }) {\n            if (!restAPIEnabled) {\n                return yield* new RestAPIError({ error: 'Endpoint not found' });\n            }\n            const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);\n\n            if (user.rank !== 'owner' \u0026\u0026 user.rank !== 'admin') {\n                return yield* new RestAPIError({ error: 'Unauthorized' });\n            }\n\n            const allUsers = yield* sdk.GET.users.all();\n            let data = allUsers.map(...);\n\n            if (rank !== 'owner') {\n                data = data.filter((user) =\u003e user.rank !== 'owner');\n            }\n\n            if (rank) {\n                data = data.filter((user) =\u003e user.rank === rank);\n            }\n\n            return data;\n        },\n```\n\nThe `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.\n\n### Adjacent Endpoint Shows Intended Security Boundary\n\nFile: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1650-1710\n\n```ts\nconst existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);\nconst loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);\n\nif (loggedInUserRankIndex \u003c= existingUserRankIndex) {\n    return yield* new RestAPIError({\n        error: 'Unauthorized to view user with higher rank',\n    });\n}\n```\n\n`getUser` correctly blocks an admin from viewing an owner record. `getUsers` bypasses that boundary for bulk enumeration.\n\n### Sensitive Fields Returned\n\nThe `getUsers` response includes:\n\n- `id`\n- `email`\n- `name`\n- `username`\n- `rank`\n- timestamps and profile URL/avatar fields when present\n\nThis is enough to enumerate all owner accounts and target them for phishing, social engineering, or follow-on attacks against out-of-band workflows.\n\n## PoC\n\n### HTTP PoC\n\nUse any admin-level REST API token:\n\n```bash\ncurl -X GET 'http://localhost:4321/studiocms_api/rest/v1/secure/users?rank=owner' \\\n  -H 'Authorization: Bearer \u003cadmin-api-token\u003e'\n```\n\nExpected behavior:\n- owner records should be excluded for admin callers, consistent with `getUser`\n\nActual behavior:\n- the response contains owner user objects, including email addresses and user IDs\n\n### Local Validation of the Exact Handler Logic\n\nI validated the filtering logic locally with the same conditions used by `getUsers` and `getUser`.\n\nObserved output:\n\n```json\n{\n  \"admin_getUsers_rank_owner\": [\n    {\n      \"email\": \"owner@example.test\",\n      \"id\": \"owner-1\",\n      \"name\": \"Site Owner\",\n      \"rank\": \"owner\",\n      \"username\": \"owner1\"\n    }\n  ],\n  \"admin_getUser_owner\": \"Unauthorized to view user with higher rank\"\n}\n```\n\nThis demonstrates the authorization mismatch clearly:\n- bulk listing with `rank=owner` exposes owner records\n- direct access to a single owner record is denied\n\n## Impact\n\n- **Owner Account Enumeration:** Admin tokens can recover owner user IDs, usernames, display names, and email addresses.\n- **Authorization Boundary Bypass:** The REST collection endpoint bypasses the stricter per-record rank check already implemented by `getUser`.\n- **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.\n\n## Recommended Fix\n\nApply rank filtering based on the caller's role, not on the request query parameter, and reuse the same privilege rule as `getUser`.\n\nExample fix:\n\n```ts\nconst loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);\n\ndata = data.filter((candidate) =\u003e {\n    const candidateRankIndex = availablePermissionRanks.indexOf(candidate.rank);\n    return loggedInUserRankIndex \u003e candidateRankIndex;\n});\n\nif (rank) {\n    data = data.filter((candidate) =\u003e candidate.rank === rank);\n}\n```\n\nAt minimum, replace:\n\n```ts\nif (rank !== 'owner') {\n    data = data.filter((user) =\u003e user.rank !== 'owner');\n}\n```\n\nwith a check tied to `user.rank` rather than the query parameter.","status":"published","published":"2026-03-16T16:37:42Z","modified":"2026-03-18T21:51:20Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low"}],"cwes":["CWE-639"],"updated_at":"2026-11-02T14:39:37.554665Z"}],"nvd":[{"id":"CVE-2026-32638","description":"StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.4, 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. Version 0.4.4 fixes the issue.","status":"published","published":"2026-03-18T21:16:26.77Z","modified":"2026-06-17T10:36:08.737Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low","assessor":"secondary"}],"cwes":["CWE-639"],"updated_at":"2026-11-02T14:12:09.735529Z"}],"osv":[{"id":"CVE-2026-32638","title":"StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens","description":"StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.4, 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. Version 0.4.4 fixes the issue.","status":"published","published":"2026-03-18T20:41:14.034Z","modified":"2026-10-28T02:30:28.227428171Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low"}],"cwes":["CWE-639"],"updated_at":"2026-11-02T15:45:01.548715Z"},{"id":"GHSA-xvf4-ch4q-2m24","title":"StudioCMS REST getUsers Exposes Owner Account Records to Admin Tokens","description":"## Summary\n\nThe 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.\n\n## Details\n\n### Vulnerable Code Path\n\nFile: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1605-1647\n\n```ts\n.handle(\n    'getUsers',\n    Effect.fn(\n        function* ({ urlParams: { name, rank, username } }) {\n            if (!restAPIEnabled) {\n                return yield* new RestAPIError({ error: 'Endpoint not found' });\n            }\n            const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);\n\n            if (user.rank !== 'owner' \u0026\u0026 user.rank !== 'admin') {\n                return yield* new RestAPIError({ error: 'Unauthorized' });\n            }\n\n            const allUsers = yield* sdk.GET.users.all();\n            let data = allUsers.map(...);\n\n            if (rank !== 'owner') {\n                data = data.filter((user) =\u003e user.rank !== 'owner');\n            }\n\n            if (rank) {\n                data = data.filter((user) =\u003e user.rank === rank);\n            }\n\n            return data;\n        },\n```\n\nThe `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.\n\n### Adjacent Endpoint Shows Intended Security Boundary\n\nFile: `D:/bugcrowd/studiocms/repo/packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts`, lines 1650-1710\n\n```ts\nconst existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);\nconst loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);\n\nif (loggedInUserRankIndex \u003c= existingUserRankIndex) {\n    return yield* new RestAPIError({\n        error: 'Unauthorized to view user with higher rank',\n    });\n}\n```\n\n`getUser` correctly blocks an admin from viewing an owner record. `getUsers` bypasses that boundary for bulk enumeration.\n\n### Sensitive Fields Returned\n\nThe `getUsers` response includes:\n\n- `id`\n- `email`\n- `name`\n- `username`\n- `rank`\n- timestamps and profile URL/avatar fields when present\n\nThis is enough to enumerate all owner accounts and target them for phishing, social engineering, or follow-on attacks against out-of-band workflows.\n\n## PoC\n\n### HTTP PoC\n\nUse any admin-level REST API token:\n\n```bash\ncurl -X GET 'http://localhost:4321/studiocms_api/rest/v1/secure/users?rank=owner' \\\n  -H 'Authorization: Bearer \u003cadmin-api-token\u003e'\n```\n\nExpected behavior:\n- owner records should be excluded for admin callers, consistent with `getUser`\n\nActual behavior:\n- the response contains owner user objects, including email addresses and user IDs\n\n### Local Validation of the Exact Handler Logic\n\nI validated the filtering logic locally with the same conditions used by `getUsers` and `getUser`.\n\nObserved output:\n\n```json\n{\n  \"admin_getUsers_rank_owner\": [\n    {\n      \"email\": \"owner@example.test\",\n      \"id\": \"owner-1\",\n      \"name\": \"Site Owner\",\n      \"rank\": \"owner\",\n      \"username\": \"owner1\"\n    }\n  ],\n  \"admin_getUser_owner\": \"Unauthorized to view user with higher rank\"\n}\n```\n\nThis demonstrates the authorization mismatch clearly:\n- bulk listing with `rank=owner` exposes owner records\n- direct access to a single owner record is denied\n\n## Impact\n\n- **Owner Account Enumeration:** Admin tokens can recover owner user IDs, usernames, display names, and email addresses.\n- **Authorization Boundary Bypass:** The REST collection endpoint bypasses the stricter per-record rank check already implemented by `getUser`.\n- **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.\n\n## Recommended Fix\n\nApply rank filtering based on the caller's role, not on the request query parameter, and reuse the same privilege rule as `getUser`.\n\nExample fix:\n\n```ts\nconst loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);\n\ndata = data.filter((candidate) =\u003e {\n    const candidateRankIndex = availablePermissionRanks.indexOf(candidate.rank);\n    return loggedInUserRankIndex \u003e candidateRankIndex;\n});\n\nif (rank) {\n    data = data.filter((candidate) =\u003e candidate.rank === rank);\n}\n```\n\nAt minimum, replace:\n\n```ts\nif (rank !== 'owner') {\n    data = data.filter((user) =\u003e user.rank !== 'owner');\n}\n```\n\nwith a check tied to `user.rank` rather than the query parameter.","status":"published","published":"2026-03-16T16:37:42Z","modified":"2026-03-18T22:01:30.322032Z","cvss":[{"version":"3.1","score":2.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","level":"low"}],"cwes":["CWE-639"],"updated_at":"2026-11-02T15:47:00.604718Z"}]}}
