{"schema":"threatline.record/2","record":{"id":"GHSA-crh2-cv2j-cpjh","aliases":["GHSA-crh2-cv2j-cpjh"],"summary":"phpMyFAQ deleteGroup API lacks a privilege check, letting a non-SuperAdmin GROUP_DELETE admin destroy higher-privileged groups","details":"## Summary\r\n\r\nphpMyFAQ's admin API endpoint for deleting a user group (`POST /admin/api/group/delete`, handled by `GroupController::deleteGroup()`) enforces only a coarse binary permission check that the caller holds the delegable `GROUP_DELETE` right. It performs no privilege-level comparison between the acting administrator and the target group. As a result, an authenticated, non-SuperAdmin administrator who holds `GROUP_DELETE` can permanently destroy any group, including groups that carry high-privilege rights (for example configuration editing or backup) that the attacker does not hold and cannot otherwise grant, join, or manage. Every other group-write endpoint in the same controller gates the operation on a privilege-level check; group deletion is the sole exception.\r\n\r\n## Root Cause\r\n\r\nThe application's group-write operations are designed to enforce a privilege-containment invariant: a non-SuperAdmin may only operate on a group whose rights the acting user fully holds, preventing privilege escalation through group membership inheritance. This invariant is enforced inline by the sibling write endpoints but is entirely absent from the deletion endpoint.\r\n\r\n`deleteGroup()` calls `userHasPermission(PermissionType::GROUP_DELETE)`, which resolves to a binary right-ownership check that does not consider the target group's privilege level. It then passes the attacker-controlled `groupId` directly to `MediumPermission::deleteGroup()`, which executes unconditional `DELETE` statements against the group, its memberships, its category and language restrictions, and its right assignments. No layer between the controller entry and the SQL sink compares the acting user's privileges against the target group's rights.\r\n\r\n```text\r\nVulnerable endpoint (paths relative to the repository root):\r\n  phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/GroupController.php\r\n    - deleteGroup(): gate is userHasPermission(GROUP_DELETE) only; no privilege-level check\r\n    - sink: $currentUser->perm->deleteGroup($groupId)\r\n  phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php\r\n    - deleteGroup(): raw DELETE statements, no authorization check\r\n\r\nSibling endpoints in the same controller that DO enforce the invariant:\r\n  updateGroup(), updateMembers(), updatePermissions() each check\r\n  isSuperAdmin() plus a per-right hasPermission() loop over the target group's rights.\r\n```\r\n\r\n## Impact\r\n\r\nAn authenticated administrator who holds the delegable `GROUP_DELETE` right, but who is not a SuperAdmin and does not hold the target group's rights, can permanently delete any group in the installation. Deleting a group destroys that group's permission grants, its member associations, and its category and language restrictions. Members of the deleted group immediately lose every right they inherited through it.\r\n\r\nThe attacker gains a destructive capability over privilege infrastructure that exceeds their own authority: they cannot grant themselves the target group's rights, cannot add themselves to the group, and cannot enable auto-join on it (each of those paths is privilege-gated), yet they can destroy the group outright. The demonstrated impact is unauthorized destruction of group records and the associated rights of other users; it does not by itself grant the attacker any new right or read access. Group permissions mode (the medium permission model) must be active for the endpoint to operate.\r\n\r\n## Proof of Concept\r\n\r\n```text\r\nPreconditions:\r\n  - phpMyFAQ configured in medium permission mode (group permissions enabled).\r\n  - Attacker authenticated as an administrator holding the GROUP_DELETE right,\r\n    but not a SuperAdmin, and not holding the rights of the target group.\r\n  - Attacker holds a valid admin session and CSRF token (readable from any\r\n    admin page the attacker can already load).\r\n  - A target group (for example group id 5) exists that carries higher-privilege\r\n    rights the attacker lacks (for example configuration editing, backup).\r\n```\r\n\r\nReproduction:\r\n\r\n1. Authenticate to the admin panel as the non-SuperAdmin administrator holding `GROUP_DELETE`.\r\n2. Read the CSRF token for the group-delete action from an admin page.\r\n3. Send the deletion request against the higher-privileged group id.\r\n\r\n```http\r\nPOST /admin/api/group/delete HTTP/1.1\r\nHost: <target>\r\nCookie: <authenticated admin session>\r\nContent-Type: application/json\r\n\r\n{\"csrfToken\": \"<valid token>\", \"groupId\": 5}\r\n```\r\n\r\n```text\r\nObserved result: HTTP 200 with the group-deleted success message.\r\nGroup 5 and all of its rows in faqgroup, faquser_group, faqgroup_right, and its\r\ncategory/language restriction tables are removed. Members of group 5 lose the\r\nrights they inherited from it, even though the acting administrator could not\r\ngrant, join, or manage that group through any privilege-gated endpoint.\r\n```\r\n\r\n## Attack Chain\r\n\r\n1. **Exposure:** The admin API route `POST /admin/api/group/delete` ships enabled and is reachable by any authenticated administrator session.\r\n2. **Control:** The attacker controls the JSON body, including `groupId`, which is validated only as a positive integer.\r\n3. **Path:** The request reaches `GroupController::deleteGroup()`, passes the CSRF check with the attacker's valid token, and passes the medium-permission-mode instance check.\r\n4. **Guard:** The only authorization gate is `userHasPermission(GROUP_DELETE)`, a binary right-ownership check. The privilege-level comparison that the sibling write endpoints apply is missing entirely.\r\n5. **Primitive:** The attacker-controlled `groupId` flows to `MediumPermission::deleteGroup()`, which executes unconditional `DELETE` statements across the group's records and rights.\r\n6. **Result:** Any group, including one holding rights the attacker cannot obtain, is permanently destroyed, and its members lose those inherited rights.\r\n\r\n## Bypass Evidence\r\n\r\nThe relevant guard is the privilege-containment check that gates group-write operations. In the published artifact, `updateGroup()`, `updateMembers()`, and `updatePermissions()` each enforce it inline: for a non-SuperAdmin caller they iterate the target group's rights and reject the operation unless the acting user holds every one of those rights. Their inline comments document the exact threat model, that a delegable group-management right must not let an administrator reach privileges they do not already hold.\r\n\r\n`deleteGroup()` omits this check. It runs only `userHasPermission(GROUP_DELETE)` before invoking the destructive sink. The following disproof attempts were made and each failed to invalidate the finding:\r\n\r\n- Checked whether `GROUP_DELETE` is effectively SuperAdmin-equivalent: it is a narrow, individually delegable right in the permission enumeration, distinct from configuration, backup, and group-administration rights, so holding it does not already grant the destroyed capability.\r\n- Checked for a compensating guard deeper in the stack: `MediumPermission::deleteGroup()`, its repository layer, `AbstractController::userHasPermission()`, the administration API base controller, and the kernel event listeners were all inspected; none performs a privilege-level comparison for this operation.\r\n- Checked whether the capability is merely denial rather than a boundary crossing: the attacker can destroy a group they cannot create, join, or manage through any authorized path, which is a capability beyond their granted authority.\r\n- Checked whether front-end restrictions or the audit log prevent exploitation: the API is called directly, and the audit-log entry is written after the deletion, so it is detection rather than prevention.\r\n\r\n## Affected Versions\r\n\r\n- `Ecosystem: composer`\r\n- `Package: thorsten/phpmyfaq`\r\n- `Confirmed affected range: >= 4.2.0-alpha, <= 4.2.0-alpha.2`\r\n- `Latest release checked: 4.2.0-alpha.2 (Composer / Packagist, published 2026-08-20)`\r\n- `Fix status: not fixed`\r\n\r\nThe group-write endpoints, including `deleteGroup()`, were introduced during the 4.2.0 development cycle. The last stable release, 4.1.8, is not affected: its `GroupController` exposes only read-only endpoints and contains no group-deletion method. Both published pre-releases, 4.2.0-alpha and 4.2.0-alpha.2, were inspected directly and both ship the vulnerable endpoint without a privilege-level check. The introduction boundary is 4.2.0-alpha; no published or later development artifact yet contains a fix. The same Composer artifact is also published under the coordinate `phpmyfaq/phpmyfaq`.\r\n\r\n## Suggested Fix\r\n\r\nEnforce the same privilege-containment invariant on group deletion that the sibling group-write endpoints already apply. Before deleting, require that the acting user is a SuperAdmin, or that the acting user holds every right carried by the target group; reject the request otherwise. The check must live in the deletion controller path (or, more robustly, inside the permission layer's `deleteGroup()` so every caller is covered), not in the front-end. As an interim mitigation, operators can restrict the `GROUP_DELETE` right to fully trusted SuperAdmin-equivalent administrators until a code fix is available.\r\n\r\nReported by zx (GitHub: @manus-pi).\r\n","categories":["Unclassified"],"product":"thorsten/phpmyfaq","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-crh2-cv2j-cpjh","source_modified_at":"2026-10-03T11:28:19Z","fetched_at":"2026-10-28T01:10:53.534015Z","package":{"ecosystem":"composer","name":"thorsten/phpmyfaq"},"range":">= 4.2.0-alpha, <= 4.2.0-alpha.2"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-crh2-cv2j-cpjh","source_modified_at":"2026-10-03T11:28:19Z","fetched_at":"2026-10-28T01:10:53.534015Z","package":{"ecosystem":"composer","name":"thorsten/phpmyfaq"},"version":"4.2.0-beta"}],"cvss":5.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T11:28:19Z","modified_at":"2026-10-03T11:28:19Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-crh2-cv2j-cpjh","fetched_at":"2026-10-28T01:10:53.534015Z","source_modified_at":"2026-10-03T11:28:19Z"}],"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:28:19Z","content_hash":"de6ef3fcd1fbab0f824f06f30f6556e0cfa27efe682ad02f8690e2f0a18537f3"}}
