GHSA-pg62-f8g4-4wqh: CSRF

Published Aug 25, 2026
·
Updated

Overview

When phpMyFAQ hardened its admin permission-assignment endpoints against privilege escalation, it added a "a non-SuperAdmin may only assign rights they themselves hold" constraint to the user-rights endpoint (UserController::updateUserRights). The equivalent group-rights endpoint, GroupController::updatePermissions, did not receive that constraint. A delegated administrator holding only the GROUPEDIT permission can therefore grant any group an arbitrary set of rights — including rights the administrator does not possess — and, by being (or becoming) a member of that group, inherit those rights, escalating to higher privileges up to full administrative control.

Impact

phpMyFAQ supports delegated administration: the GROUPEDIT right can be granted to a non-SuperAdmin so they can manage groups. Such an administrator can escalate:

1. They call POST /admin/group/update/permissions with groupid set to a group they belong to (or can manage membership of) and grouprights[] containing high-value rights they do not themselves hold (e.g. user administration, or any right gating sensitive actions). 2. The endpoint grants every requested right to the group with no check that the caller holds them. 3. Members of that group — including the attacker — inherit the granted rights, escalating the attacker's effective privileges.

This is the group-side mirror of exactly what the maintainers blocked on the user-rights side, where the code comment names the threat explicitly ("prevents an administrator with the delegable USEREDIT right from granting privileges they do not possess (privilege escalation)"). The group path remains open.

PR:L (the attacker needs the delegable GROUPEDIT right, below SuperAdmin), S:U (escalation within phpMyFAQ's single authorization authority), C:H/I:H/A:H (inherited rights can reach full administrative read/write/availability control). The one added step versus the user-rights path — the attacker must be a member of the group they elevate (a GROUPEDIT admin generally manages group membership, hence AC:L) — is noted in Technical Details.

Technical Details

References are to phpmyfaq/src/phpMyFAQ/ at HEAD 04db2b999d8d.

The vulnerable endpoint — no self-rights check (Controller/Administration/GroupController.php:309-349):

php #[Route(path: '/group/update/permissions', name: 'admin.group.update.permissions', methods: ['POST'])] public function updatePermissions(Request $request): Response { $this->userHasPermission(PermissionType::GROUPEDIT); // only requires GROUPEDIT — not SuperAdmin, no per-right check // ... CSRF verified ... $groupId = (int) Filter::filterVar($request->request->get('groupid'), FILTERVALIDATEINT); $groupPermissions = $request->request->all()['grouprights']; // attacker-controlled list of right IDs

$refuseResult = $this->user->perm->refuseAllGroupRights($groupId); if ($refuseResult) { foreach ($groupPermissions as $groupPermission) { $this->user->perm->grantGroupRight($groupId, (int) $groupPermission); // grants ANY right, unconstrained } ... } }

Each grouprights[] entry is granted to the group verbatim; there is no verification that the acting administrator holds that right.

The fixed sibling — updateUserRights DOES constrain to self-held rights (Controller/Administration/Api/UserController.php:558-579):

php $actingIsSuperAdmin = $this->currentUser->isSuperAdmin(); // A non-SuperAdmin may only assign rights they hold themselves. This prevents an // administrator with the delegable USEREDIT right from granting privileges they do not // possess (privilege escalation). if (!$actingIsSuperAdmin) { $actingUserId = $this->currentUser->getUserId(); foreach ($userRights as $userRight) { if (!$this->currentUser->perm->hasPermission($actingUserId, (int) $userRight)) { return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTPFORBIDDEN); } } }

The identical "may only assign rights you hold" loop is present for user rights but absent for group rights. The group endpoint's only gate is userHasPermission(GROUPEDIT) (Controller/AbstractController.php/AbstractAdministrationController.php), which checks the caller holds GROUPEDIT — not that they hold each right being granted. This is an authorization omission: the enforcer the maintainers already wrote for the analogous mass-assignment of rights is simply not applied on the group path.

Inheritance step (honest precondition). grantGroupRight grants the right to the group; the attacker realizes the escalation by being a member of the elevated group. A GROUPEDIT administrator manages groups (and typically their membership), so they can target a group they already belong to or add themselves — keeping AC:L. If a given deployment separates group-membership management from GROUPEDIT, the attacker is limited to elevating groups they already belong to, which is still the common case for a delegated group admin.

Reproduction

phpMyFAQ is self-hosted; reproduce on your own test instance. Create a non-SuperAdmin account granted GROUPEDIT (and member of some group G), log in as it, and run in the DevTools Console:

js // Run as the delegated (non-SuperAdmin) GROUPEDIT admin, on the phpMyFAQ admin UI. // Grant group G (a group the attacker belongs to) a right the attacker does NOT hold // (use a numeric RIGHTID for a high-value permission the account lacks, e.g. user admin). const csrf = document.querySelector('[name="pmf-csrf-token"], #pmf-csrf-token')?.value || window.PMFCSRFUPDATEGROUPPERMISSIONS; // the update-group-permissions token rendered on the group page const body = new URLSearchParams(); body.set("pmf-csrf-token", csrf); body.set("groupid", String(/ G's group id / 2)); body.append("grouprights[]", String(/ RIGHTID the attacker lacks / 1)); fetch("/admin/group/update/permissions", { method: "POST", credentials: "include", body }) .then((r) => r.text()) .then((t) => console.log(t.includes("savedsuc") ? "GRANTED (200)" : t.slice(0, 200)));

Expected result: the response reports success (admsgsavedsuc), i.e. the right was granted to group G even though the acting admin does not hold it. Confirm with SELECT FROM faqgroupright WHERE groupid=2 (or the app's group-rights view) that the new rightid is present, then verify the attacker (a member of G) now exercises the inherited right. (Against updateUserRights the same attempt to assign an unheld right returns 403 msgNoPermission, demonstrating the missing constraint is specific to the group path.)

End-to-end (source) verification

Authorization-omission finding; the gap is open at HEAD and reaches the privilege-grant sink with no intervening per-right check:

- Endpoint reachable by a delegated non-SuperAdmin: updatePermissions gate is userHasPermission(PermissionType::GROUPEDIT) (:311) — confirmed it does not require SuperAdmin nor check the granted rights. - Attacker controls the granted rights: grouprights[] from the request body, granted in the loop at :329. - Missing control: the self-rights loop present in updateUserRights (UserController.php:563-571) has no counterpart in updatePermissions — verified by reading both handlers at HEAD. - Sink: MediumPermission::grantGroupRight -> INSERT INTO faqgroupright (Permission repository), persisting the unheld right to the group; members of the group inherit it.

Suggested Fix

Apply the same self-rights constraint updateUserRights already has, before granting:

php $actingIsSuperAdmin = $this->currentUser->isSuperAdmin(); if (!$actingIsSuperAdmin) { $actingUserId = $this->currentUser->getUserId(); foreach ($groupPermissions as $groupPermission) { if (!$this->currentUser->perm->hasPermission($actingUserId, (int) $groupPermission)) { throw new UnauthorizedHttpException('Cannot grant a right you do not hold'); } } }

More robustly, factor the "you may only assign rights you hold" rule into the permission layer (grantGroupRight / grantUserRight) so both the user-rights and group-rights paths enforce it uniformly, and add a regression test mirroring testUpdateRightsNonSuperAdminCannotGrantRightTheyDoNotHold for the group endpoint. (See also the related sibling gaps in the same admin-API authorization series: UserController::addUser missing the acting-SuperAdmin guard, and the user/data / user/permissions target-authorization on read.)

Disclosure Timeline

- 2026-05-30: Discovered while auditing the completeness of the GHSA-xvp4 / GHSA-985r / GHSA-8c6h admin-API authorization-hardening series at main HEAD 04db2b999d8d. The self-rights constraint added to updateUserRights was confirmed absent on the GroupController::updatePermissions sibling by reading both handlers + the permission gate + the grantGroupRight sink. - 2026-05-30: Drafted for submission via GitHub Security Advisory.

References

- Hardening series this incompletely fixes: GHSA-xvp4-phqj-cjr3, GHSA-985r-q3qp-299h ("incomplete fix for GHSA-xvp4"), GHSA-8c6h-7g6x-m5x4 ("incomplete fix for CVE-2026-24421"). - Affected source: phpmyfaq/src/phpMyFAQ/Controller/Administration/GroupController.php:309-349 (updatePermissions, gate userHasPermission(GROUPEDIT) at :311, sink grantGroupRight at :329); fixed sibling Controller/Administration/Api/UserController.php:534-589 (updateUserRights, self-rights loop at :563-566); Controller/AbstractController.php:326-336 (userHasPermission, the only gate — GROUPEDIT action permission). - Companion advisory (same audit, same series): user/add missing acting-SuperAdmin guard -> delegated admin creates SuperAdmin (8.8 High).

Affected Software

2 affected componentsFixes available
composer/thorsten/phpmyfaq<=4.1.4
4.1.5
composer/phpmyfaq/phpmyfaq<=4.1.4
4.1.5

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/thorsten/phpmyfaq to a version that resolves this vulnerability.

    Fixed in 4.1.5
  2. Upgrade

    Upgrade composer/phpmyfaq/phpmyfaq to a version that resolves this vulnerability.

    Fixed in 4.1.5
  3. Configuration

    Before calling grantGroupRight in GroupController::updatePermissions, add the same per-right check present in UserController::updateUserRights (Controller/Administration/Api/UserController.php:563-566): if the acting user is not SuperAdmin, reject any right_id in request body (group_rights[]) that the acting admin does not hold (e.g., by throwing/returning the 'Cannot grant a right you do not hold' / msgNoPermission behavior).

    phpMyFAQ GroupController::updatePermissions (Controller/Administration/GroupController.php:309-349) Self-rights constraint for granted group_rights[] = Enforce 'you may only assign rights you hold' for each right_id in group_rights[] (mirror updateUserRights)

Event History

Aug 25, 2026
Advisory Published
via GitHub·05:32 PM
Data Sourced
via GitHub·05:32 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which accounts can exploit this issue?

A non-SuperAdmin account with the GROUP_EDIT permission can exploit it. The account must be a member of the target group or be able to manage that group's membership so it can inherit the rights assigned to the group.

2

What does an attacker need to do to escalate privileges?

The attacker sends a POST request to /admin/group/update/permissions with a target group_id and group_rights[] containing permissions they do not hold. The affected endpoint grants the requested rights without verifying that the delegated administrator already possesses them.

3

Is the user-rights permission-assignment endpoint affected in the same way?

The user-rights endpoint, UserController::updateUserRights, has a restriction preventing non-SuperAdmins from assigning rights they do not hold. The missing equivalent restriction is identified for GroupController::updatePermissions.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203