{"schema":"threatline.record/2","record":{"id":"GHSA-gv7w-78f6-mcmh","aliases":["GHSA-gv7w-78f6-mcmh"],"summary":"Privilege Escalation in Group Category Restrictions Due to Missing Delegated-Admin Scope Validation","details":"Summary :\r\n\r\nphpMyFAQ's administrative API allows a delegated administrator to modify group category restrictions without verifying that the categories being assigned fall within the administrator's own authorized category scope.\r\n\r\nThe application correctly enforces this rule when assigning language restrictions, but the equivalent category-restriction endpoint lacks a corresponding authorization guard.\r\n\r\nAs a result, a non-super administrator who already possesses the required group-management permissions may grant a group access to categories outside the administrator's own scope.\r\n\r\nThe issue is especially significant because submitting an empty `categoryIds` array is interpreted as unrestricted access, which can cause a group permission to apply to all categories.\r\n\r\nSeverity :\r\n\r\nMedium\r\n\r\nCVSS 3.1:\r\n\r\n`CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N`\r\n\r\nScore: **6.0 (Medium)**\r\n\r\nCWE:\r\n\r\n* CWE-269 — Improper Privilege Management\r\n* CWE-639 — Authorization Bypass Through User-Controlled Key / Improper Object Authorization\r\n\r\nConfidence:\r\n\r\n**High**\r\n\r\nAffected Version :\r\n\r\nphpMyFAQ **4.2.x**\r\n\r\nConfirmed on:\r\n\r\n`phpMyFAQ 4.2.0-beta`\r\n\r\nReviewed Git commit:\r\n\r\n`0fb7ecd`\r\n\r\nAffected Component :\r\n\r\nAdministrative REST API — group category restrictions.\r\n\r\nAffected file:\r\n\r\n`phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/GroupController.php`\r\n\r\nAffected function:\r\n\r\n`GroupController::saveCategoryRestrictions()`\r\n\r\nRelevant route:\r\n\r\n`POST /admin/api/group/category-restrictions`\r\n\r\nDescription :\r\n\r\nphpMyFAQ supports delegated administrators whose permissions may be restricted to specific categories.\r\n\r\nAn important security invariant in this model is:\r\n\r\nA delegated administrator must not be allowed to grant another user or group access to resources that the administrator does not personally have access to.\r\n\r\nThe application already enforces a similar rule when modifying language restrictions.\r\n\r\nFor example, the language-restriction flow invokes a scope-validation function before persisting permissions.\r\n\r\nHowever, the equivalent category restriction endpoint does not perform an equivalent validation.\r\n\r\nThe vulnerable code processes attacker-controlled category identifiers:\r\n\r\n```php\r\n$categoryIds = array_values(\r\n    array_filter(\r\n        array_map('intval', $rawCategoryIds),\r\n        fn(int $id) => $id > 0\r\n    )\r\n);\r\n```\r\n\r\nand subsequently writes the restrictions directly:\r\n\r\n```php\r\n$success = $currentUser->perm->setCategoryRestrictions(\r\n    $groupId,\r\n    $rightId,\r\n    $categoryIds\r\n);\r\n```\r\n\r\nThere is no equivalent of:\r\n\r\n```text\r\nmayAssignCategories()\r\n```\r\n\r\nbefore the permission update.\r\n\r\nThis means the endpoint verifies that the administrator has general group-management privileges, but does not verify whether the administrator is authorized to grant the specific category scope being assigned.\r\n\r\nThe related language-restriction implementation does enforce this kind of rule using:\r\n\r\n```text\r\nmayAssignLanguages()\r\n```\r\n\r\nwhich demonstrates that scope-limiting delegated administrators is an intended security control.\r\n\r\nThe category implementation lacks the equivalent control entirely.\r\n\r\nProof of Concept :\r\n\r\nAssume the following permission model:\r\n\r\n```text\r\nDelegated administrator:\r\nAllowed categories: [1]\r\n\r\nTarget group:\r\nHas a permission/right R\r\n```\r\n\r\nThe delegated administrator should only be able to restrict the group's permission to:\r\n\r\n```text\r\ncategory 1\r\n```\r\n\r\nand should not be able to grant:\r\n\r\n```text\r\ncategory 2\r\ncategory 3\r\nall categories\r\n```\r\n\r\nHowever, the administrator can submit a request to:\r\n\r\n```text\r\nPOST /admin/api/group/category-restrictions\r\n```\r\n\r\nwith a category outside the administrator's scope, for example:\r\n\r\n```json\r\n{\r\n  \"groupId\": 10,\r\n  \"rightId\": 5,\r\n  \"categoryIds\": [2]\r\n}\r\n```\r\n\r\nor more significantly:\r\n\r\n```json\r\n{\r\n  \"groupId\": 10,\r\n  \"rightId\": 5,\r\n  \"categoryIds\": []\r\n}\r\n```\r\n\r\nThe empty category list is treated as unrestricted.\r\n\r\nTherefore, the group's right may become applicable to every category even though the delegated administrator is personally restricted to a smaller category set.\r\n\r\nSteps To Reproduce :\r\n\r\n1. Deploy phpMyFAQ 4.2.x in a controlled environment.\r\n\r\n2. Enable group-based permissions.\r\n\r\nThe relevant configuration requires:\r\n\r\n```text\r\nsecurity.permLevel != basic\r\n```\r\n\r\n3. Create at least two categories:\r\n\r\n```text\r\nCategory 1\r\nCategory 2\r\n```\r\n\r\n4. Create a non-super administrator.\r\n\r\n5. Give the administrator the required management permissions:\r\n\r\n```text\r\nUSER_ADD\r\nUSER_EDIT\r\nUSER_DELETE\r\nGROUP_EDIT\r\n```\r\n\r\n6. Restrict this administrator to:\r\n\r\n```text\r\nCategory 1\r\n```\r\n\r\n7. Create a target group.\r\n\r\n8. Assign the target group a permission/right that supports category restrictions.\r\n\r\n9. Authenticate as the restricted delegated administrator.\r\n\r\n10. Send:\r\n\r\n```text\r\nPOST /admin/api/group/category-restrictions\r\n```\r\n\r\nwith a category outside the administrator's scope:\r\n\r\n```json\r\n{\r\n  \"groupId\": \"<target-group>\",\r\n  \"rightId\": \"<target-right>\",\r\n  \"categoryIds\": [2]\r\n}\r\n```\r\n\r\n11. Observe that the server accepts and stores the category restriction even though the acting administrator does not have access to Category 2.\r\n\r\n12. Alternatively, submit:\r\n\r\n```json\r\n{\r\n  \"groupId\": \"<target-group>\",\r\n  \"rightId\": \"<target-right>\",\r\n  \"categoryIds\": []\r\n}\r\n```\r\n\r\n13. The empty category list results in the permission becoming unrestricted across categories.\r\n\r\n14. Authenticate as a member of the target group.\r\n\r\n15. Verify that the group member can access or use the affected permission in a category that was not accessible to the delegated administrator who assigned it.\r\n\r\nImpact :\r\n\r\nA delegated administrator can escalate the effective privileges of a group beyond the administrator's own authorization boundary.\r\n\r\nPotential consequences include:\r\n\r\n**Authorization Boundary Bypass**\r\n\r\nA category-restricted administrator may assign group access to categories outside their own scope.\r\n\r\n**Indirect Privilege Escalation**\r\n\r\nThe administrator can grant broader access to another group and then potentially use or add accounts within that group depending on other granted permissions.\r\n\r\n**Unrestricted Category Access**\r\n\r\nSubmitting:\r\n\r\n```json\r\n\"categoryIds\": []\r\n```\r\n\r\nmay make a permission unrestricted across all categories.\r\n\r\nThis is particularly dangerous because the empty array does not mean:\r\n\r\n```text\r\nno categories\r\n```\r\n\r\nbut instead represents:\r\n\r\n```text\r\nall categories / unrestricted\r\n```\r\n\r\nfor the affected permission model.\r\n\r\n**Access to Restricted Content**\r\n\r\nThe affected group may receive read, publish, or other category-specific capabilities in areas that the delegated administrator was explicitly prevented from administering.\r\n\r\nThe report notes that category restrictions are actively used by security-sensitive authorization checks including:\r\n\r\n```text\r\nuserHasPermissionForCategories\r\nuserMayPublishIn\r\nFaq/ReadScope\r\n```\r\n\r\nTherefore, bypassing the category scope can have direct confidentiality and integrity consequences.\r\n\r\nRoot Causes :\r\n\r\nThe primary root cause is a missing authorization check in:\r\n\r\n```text\r\nGroupController::saveCategoryRestrictions()\r\n```\r\n\r\nThe endpoint validates the administrator's general group-level permissions but does not verify the administrator's category scope before calling:\r\n\r\n```php\r\nsetCategoryRestrictions(...)\r\n```\r\n\r\nThe implementation therefore confuses two different authorization questions:\r\n\r\n```text\r\n\"Is this user allowed to edit groups?\"\r\n```\r\n\r\nand:\r\n\r\n```text\r\n\"Is this user allowed to grant this specific category scope?\"\r\n```\r\n\r\nThe first is checked.\r\n\r\nThe second is not.\r\n\r\nThe vulnerability is also caused by inconsistent authorization design.\r\n\r\nThe language-restriction endpoint correctly performs a delegated-scope validation using:\r\n\r\n```text\r\nmayAssignLanguages()\r\n```\r\n\r\nwhile no equivalent:\r\n\r\n```text\r\nmayAssignCategories()\r\n```\r\n\r\nexists in the codebase.\r\n\r\nA further risk is the semantic meaning of an empty category list.\r\n\r\nFor delegated administrators:\r\n\r\n```text\r\ncategoryIds = []\r\n```\r\n\r\nmust not be accepted when their own permission is restricted, because the empty list removes the restriction entirely.\r\n\r\nSuggested Fix :\r\n\r\nAdd a dedicated category-scope validation method to the administrative API base controller.\r\n\r\nFor example:\r\n\r\n```php\r\nprotected function mayAssignCategories(\r\n    int $rightId,\r\n    array $categoryIds\r\n): bool {\r\n    // Super-admin may assign any scope.\r\n    if ($this->currentUser->isSuperAdmin()) {\r\n        return true;\r\n    }\r\n\r\n    $allowedCategories =\r\n        $this->currentUser->perm->getCategoryRestrictions($rightId);\r\n\r\n    // null/unrestricted means the acting user may assign any category.\r\n    if ($allowedCategories === null) {\r\n        return true;\r\n    }\r\n\r\n    // Empty category list would mean unrestricted.\r\n    // A restricted delegated admin must not be allowed to assign this.\r\n    if ($categoryIds === []) {\r\n        return false;\r\n    }\r\n\r\n    foreach ($categoryIds as $categoryId) {\r\n        if (!in_array($categoryId, $allowedCategories, true)) {\r\n            return false;\r\n        }\r\n    }\r\n\r\n    return true;\r\n}\r\n```\r\n\r\nThen enforce the validation before writing the new restriction:\r\n\r\n```php\r\nif (!$this->mayAssignCategories($rightId, $categoryIds)) {\r\n    return $this->json(\r\n        ['error' => Translation::get('msgNoPermission')],\r\n        Response::HTTP_FORBIDDEN\r\n    );\r\n}\r\n\r\n$success = $currentUser->perm->setCategoryRestrictions(\r\n    $groupId,\r\n    $rightId,\r\n    $categoryIds\r\n);\r\n```\r\n\r\nThe implementation should mirror the existing language-scope protection.\r\n\r\nAt minimum:\r\n\r\n* Super-admins may assign arbitrary category scopes.\r\n* Unrestricted administrators may assign arbitrary category scopes.\r\n* Restricted administrators must only assign categories within their own scope.\r\n* Restricted administrators must not be able to submit an empty category array if that means unrestricted access.\r\n* Requests attempting to grant out-of-scope categories should return HTTP `403 Forbidden`.\r\n\r\nA regression test should verify:\r\n\r\nGiven an administrator restricted to:\r\n\r\n```text\r\nCategory 1\r\n```\r\n\r\nthe following request must be rejected:\r\n\r\n```json\r\n{\r\n  \"categoryIds\": [2]\r\n}\r\n```\r\n\r\nThe following request must also be rejected:\r\n\r\n```json\r\n{\r\n  \"categoryIds\": []\r\n}\r\n```\r\n\r\nA super-admin should still be allowed to perform both operations where appropriate.\r\n\r\nReferences :\r\n\r\nAffected source file:\r\n\r\n`phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/GroupController.php`\r\n\r\nAffected function:\r\n\r\n`GroupController::saveCategoryRestrictions()`\r\n\r\nRelevant locations:\r\n\r\n`GroupController.php:183-232`\r\n\r\nPrimary vulnerable write:\r\n\r\n`GroupController.php:223`\r\n\r\nRelated secure implementation for comparison:\r\n\r\n`GroupController::saveLanguageRestrictions()`\r\n\r\nRelevant language-scope guard:\r\n\r\n`GroupController.php:310-316`\r\n\r\nRelated authorization helper:\r\n\r\n`AbstractAdministrationApiController::mayAssignLanguages()`\r\n\r\nRelevant helper location:\r\n\r\n`AbstractAdministrationApiController.php:57-85`\r\n\r\nAffected route:\r\n\r\n`POST /admin/api/group/category-restrictions`\r\n\r\nWeakness classifications:\r\n\r\n* CWE-269 — Improper Privilege Management\r\n* CWE-639 — Authorization Bypass / Improper Authorization\r\n\r\nSecurity invariant:\r\n\r\nA delegated administrator must never be able to grant a group a scope that exceeds the administrator's own effective authorization scope.\r\n","categories":["Unclassified"],"product":"phpMyFAQ","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-gv7w-78f6-mcmh","source_modified_at":"2026-10-03T11:32:48Z","fetched_at":"2026-10-26T23:45:56.352924Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"range":"<= 4.2.0-alpha.2"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-gv7w-78f6-mcmh","source_modified_at":"2026-10-03T11:32:48Z","fetched_at":"2026-10-26T23:45:56.352924Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"version":"4.2.0-beta"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T11:32:48Z","modified_at":"2026-10-03T11:32:48Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-gv7w-78f6-mcmh","fetched_at":"2026-10-26T23:45:56.352924Z","source_modified_at":"2026-10-03T11:32:48Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-03T11:32:48Z","content_hash":"abb67a998501464e0b23775209400cf8447c6ffb11f11001c21699fcacf2aacd"}}
