phpMyFAQ: Privilege Escalation in Group Category Restrictions Due to Missing Delegated-Admin Scope Validation
What fixes it
- 4.2.0-betaphpMyFAQ, phpGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary :
phpMyFAQ'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.
The application correctly enforces this rule when assigning language restrictions, but the equivalent category-restriction endpoint lacks a corresponding authorization guard.
As 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.
The 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.
Severity :
Medium
CVSS 3.1:
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N
Score: 6.0 (Medium)
CWE:
- CWE-269 — Improper Privilege Management
- CWE-639 — Authorization Bypass Through User-Controlled Key / Improper Object Authorization
Confidence:
High
Affected Version :
phpMyFAQ 4.2.x
Confirmed on:
phpMyFAQ 4.2.0-beta
Reviewed Git commit:
0fb7ecd
Affected Component :
Administrative REST API — group category restrictions.
Affected file:
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/GroupController.php
Affected function:
GroupController::saveCategoryRestrictions()
Relevant route:
POST /admin/api/group/category-restrictions
Description :
phpMyFAQ supports delegated administrators whose permissions may be restricted to specific categories.
An important security invariant in this model is:
A delegated administrator must not be allowed to grant another user or group access to resources that the administrator does not personally have access to.
The application already enforces a similar rule when modifying language restrictions.
For example, the language-restriction flow invokes a scope-validation function before persisting permissions.
However, the equivalent category restriction endpoint does not perform an equivalent validation.
The vulnerable code processes attacker-controlled category identifiers:
$categoryIds = array_values(
array_filter(
array_map('intval', $rawCategoryIds),
fn(int $id) => $id > 0
)
);and subsequently writes the restrictions directly:
$success = $currentUser->perm->setCategoryRestrictions(
$groupId,
$rightId,
$categoryIds
);There is no equivalent of:
mayAssignCategories()
before the permission update.
This 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.
The related language-restriction implementation does enforce this kind of rule using:
mayAssignLanguages()
which demonstrates that scope-limiting delegated administrators is an intended security control.
The category implementation lacks the equivalent control entirely.
Proof of Concept :
Assume the following permission model:
Delegated administrator: Allowed categories: [1] Target group: Has a permission/right R
The delegated administrator should only be able to restrict the group's permission to:
category 1
and should not be able to grant:
category 2 category 3 all categories
However, the administrator can submit a request to:
POST /admin/api/group/category-restrictions
with a category outside the administrator's scope, for example:
{
"groupId": 10,
"rightId": 5,
"categoryIds": [2]
}or more significantly:
{
"groupId": 10,
"rightId": 5,
"categoryIds": []
}The empty category list is treated as unrestricted.
Therefore, the group's right may become applicable to every category even though the delegated administrator is personally restricted to a smaller category set.
Steps To Reproduce :
- Deploy phpMyFAQ 4.2.x in a controlled environment.
- Enable group-based permissions.
The relevant configuration requires:
security.permLevel != basic
- Create at least two categories:
Category 1 Category 2
- Create a non-super administrator.
- Give the administrator the required management permissions:
USER_ADD USER_EDIT USER_DELETE GROUP_EDIT
- Restrict this administrator to:
Category 1
- Create a target group.
- Assign the target group a permission/right that supports category restrictions.
- Authenticate as the restricted delegated administrator.
- Send:
POST /admin/api/group/category-restrictions
with a category outside the administrator's scope:
{
"groupId": "<target-group>",
"rightId": "<target-right>",
"categoryIds": [2]
}- Observe that the server accepts and stores the category restriction even though the acting administrator does not have access to Category 2.
- Alternatively, submit:
{
"groupId": "<target-group>",
"rightId": "<target-right>",
"categoryIds": []
}- The empty category list results in the permission becoming unrestricted across categories.
- Authenticate as a member of the target group.
- 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.
Impact :
A delegated administrator can escalate the effective privileges of a group beyond the administrator's own authorization boundary.
Potential consequences include:
Authorization Boundary Bypass
A category-restricted administrator may assign group access to categories outside their own scope.
Indirect Privilege Escalation
The administrator can grant broader access to another group and then potentially use or add accounts within that group depending on other granted permissions.
Unrestricted Category Access
Submitting:
"categoryIds": []
may make a permission unrestricted across all categories.
This is particularly dangerous because the empty array does not mean:
no categories
but instead represents:
all categories / unrestricted
for the affected permission model.
Access to Restricted Content
The affected group may receive read, publish, or other category-specific capabilities in areas that the delegated administrator was explicitly prevented from administering.
The report notes that category restrictions are actively used by security-sensitive authorization checks including:
userHasPermissionForCategories userMayPublishIn Faq/ReadScope
Therefore, bypassing the category scope can have direct confidentiality and integrity consequences.
Root Causes :
The primary root cause is a missing authorization check in:
GroupController::saveCategoryRestrictions()
The endpoint validates the administrator's general group-level permissions but does not verify the administrator's category scope before calling:
setCategoryRestrictions(...)
The implementation therefore confuses two different authorization questions:
"Is this user allowed to edit groups?"
and:
"Is this user allowed to grant this specific category scope?"
The first is checked.
The second is not.
The vulnerability is also caused by inconsistent authorization design.
The language-restriction endpoint correctly performs a delegated-scope validation using:
mayAssignLanguages()
while no equivalent:
mayAssignCategories()
exists in the codebase.
A further risk is the semantic meaning of an empty category list.
For delegated administrators:
categoryIds = []
must not be accepted when their own permission is restricted, because the empty list removes the restriction entirely.
Suggested Fix :
Add a dedicated category-scope validation method to the administrative API base controller.
For example:
protected function mayAssignCategories(
int $rightId,
array $categoryIds
): bool {
// Super-admin may assign any scope.
if ($this->currentUser->isSuperAdmin()) {
return true;
}
$allowedCategories =
$this->currentUser->perm->getCategoryRestrictions($rightId);
// null/unrestricted means the acting user may assign any category.
if ($allowedCategories === null) {
return true;
}
// Empty category list would mean unrestricted.
// A restricted delegated admin must not be allowed to assign this.
if ($categoryIds === []) {
return false;
}
foreach ($categoryIds as $categoryId) {
if (!in_array($categoryId, $allowedCategories, true)) {
return false;
}
}
return true;
}Then enforce the validation before writing the new restriction:
if (!$this->mayAssignCategories($rightId, $categoryIds)) {
return $this->json(
['error' => Translation::get('msgNoPermission')],
Response::HTTP_FORBIDDEN
);
}
$success = $currentUser->perm->setCategoryRestrictions(
$groupId,
$rightId,
$categoryIds
);The implementation should mirror the existing language-scope protection.
At minimum:
- Super-admins may assign arbitrary category scopes.
- Unrestricted administrators may assign arbitrary category scopes.
- Restricted administrators must only assign categories within their own scope.
- Restricted administrators must not be able to submit an empty category array if that means unrestricted access.
- Requests attempting to grant out-of-scope categories should return HTTP
403 Forbidden.
A regression test should verify:
Given an administrator restricted to:
Category 1
the following request must be rejected:
{
"categoryIds": [2]
}The following request must also be rejected:
{
"categoryIds": []
}A super-admin should still be allowed to perform both operations where appropriate.
References :
Affected source file:
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/GroupController.php
Affected function:
GroupController::saveCategoryRestrictions()
Relevant locations:
GroupController.php:183-232
Primary vulnerable write:
GroupController.php:223
Related secure implementation for comparison:
GroupController::saveLanguageRestrictions()
Relevant language-scope guard:
GroupController.php:310-316
Related authorization helper:
AbstractAdministrationApiController::mayAssignLanguages()
Relevant helper location:
AbstractAdministrationApiController.php:57-85
Affected route:
POST /admin/api/group/category-restrictions
Weakness classifications:
- CWE-269 — Improper Privilege Management
- CWE-639 — Authorization Bypass / Improper Authorization
Security invariant:
A delegated administrator must never be able to grant a group a scope that exceeds the administrator's own effective authorization scope.
Text from GitHub Advisory Database
Affected versions 1
- phpMyFAQ, php<= 4.2.0-alpha.2GitHub