Skip to content

phpMyFAQ has an incomplete fix for GHSA-xvp4-phqj-cjr3 — editUser() and updateUserRights() lack authorization guards

High severity GitHub Reviewed Published May 25, 2026 in thorsten/phpMyFAQ

Package

phpmyfaq/phpmyfaq (Composer)

Affected versions

<= 4.1.3

Patched versions

4.1.4
thorsten/phpmyfaq (Composer)
<= 4.1.3
4.1.4

Description

Advisory / Disclosure

phpMyFAQ 4.1.3 — incomplete fix for the admin-API IDOR/privilege-escalation class

Target: thorsten/phpMyFAQ (composer: thorsten/phpmyfaq, phpmyfaq/phpmyfaq)
Affected: <= 4.1.3 (the 4.1.3 security fix is incomplete; siblings remain)
Class: CWE-862 Missing Authorization / CWE-269 Improper Privilege Management / CWE-639 Authorization Bypass Through User-Controlled Key
Methodology: M1 incomplete-fix audit (sibling-walk of the 4.1.3 fix for GHSA-xvp4-phqj-cjr3)
Severity: High — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (same class as the parent CVE)

Summary

phpMyFAQ 4.1.3 fixed GHSA-xvp4-phqj-cjr3 ("IDOR Account Takeover") by adding actor-authorization guards to UserController::overwritePassword(). The patch establishes a new invariant, stated in its own code comments:

"Only SuperAdmins may change other users' [attributes]. Self-service is
always allowed." and "a non-SuperAdmin must never be able to alter a
SuperAdmin or protected account."

That invariant is not enforced on two sibling endpoints in the same file, which the 4.1.3 fix left unchanged, and which carry the identical "user-controlled userIdgetUserById() → privileged mutation" primitive — but with a strictly more dangerous sink:

Endpoint Route Sink Guard in 4.1.3
overwritePassword() admin/api/user/overwrite-password changePassword() isSelf + isSuperAdmin + target-protection (patched)
editUser() admin/api/user/edit setSuperAdmin((bool)$req.is_superadmin) none (only userHasPermission(USER_EDIT))
updateUserRights() admin/api/user/update-rights grantUserRight($req.userId, …) none (only userHasPermission(USER_EDIT))

A logged-in administrator holding the delegable edit_user right — but not SuperAdmin — can therefore:

  1. Set their own (or anyone's) is_superadmin flag to true via admin/api/user/editfull privilege escalation to SuperAdmin.
  2. Grant arbitrary rights to any account via admin/api/user/update-rights.

This is exactly the threat model the parent advisory (GHSA-xvp4) calls out: "organizations with multiple admin users where not all should have SuperAdmin access."

Anchors (upstream tag 4.1.3)

  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UserController.php
    • editUser() lines 419-476; user-controlled userId at :433, user-controlled is_superadmin at :443, sink
      $user >setSuperAdmin((bool)$isSuperAdmin) at :463. Only gate: userHasPermission(PermissionType::USER_EDIT) at :422.
    • updateUserRights() lines 482-520; userId at :496, sink grantUserRight($userId, …) at :511. Only gate at :485.
    • overwritePassword() lines 419… → 228-288; the patched guards at: 254-260 and: 269-273.
  • phpmyfaq/src/phpMyFAQ/Controller/AbstractController.phpuserHasPermission() :221-227 (checks one right only; non SuperAdmins can hold it).
  • phpmyfaq/src/phpMyFAQ/User.phpsetSuperAdmin() :950-962 (UPDATE faquser SET is_superadmin=… WHERE user_id=…, no guard); isSuperAdmin() :942-945.
  • phpmyfaq/src/phpMyFAQ/Permission/BasicPermission.phphasPermission() :95-112 (SuperAdmin short-circuits true, else checkUserRight).

Evidence snapshots in this folder:
advisory/fix-diff-4.1.2-to-4.1.3.txt (proves the fix touched only overwritePassword/deleteUser) and advisory/vulnerable-siblings-4.1.3.txt (the two unguarded methods as shipped). git diff 4.1.2 4.1.3 shows no change to editUser, updateUserRights, setSuperAdmin, or grantUserRight.

Proof of Concept

poc/poc.php (run log: poc/run-log.txt). Dependency-free: it builds phpMyFAQ's real schema (copied verbatim from
src/phpMyFAQ/Instance/Database/Sqlite3.php) and executes the verbatim SQL that the shipped 4.1.3 methods run —setSuperAdmin (UPDATE), grantUserRight (INSERT), and the real hasPermission / checkUserRight / getRightId queries — to prove the primitive:

  • Seeds a SuperAdmin (admin, id=1) and a non-SuperAdmin admin (editor, id=2) granted only add_user/edit_user/delete_user.
  • Control: the patched overwritePassword guard blocks editor changing the SuperAdmin (id=1) — confirms the fix works there.
  • Exploit 1: editor (non-SuperAdmin, passes userHasPermission(edit_user)) flips their own is_superadmin 0→1 → SuperAdmin. VULNERABLE.
  • Exploit 2: editor grants the editconfig right via updateUserRights. VULNERABLE.

Run:

php poc/poc.php   # -> RESULT: VULNERABLE ... EXIT 0

PoC scope (honest)

The PoC exercises the privilege-escalation primitive (the unguarded sinks + the real authorization-resolution logic) against the real schema. The full HTTP exploit additionally requires an authenticated admin session and a CSRF token (editUser verifies update-user-data, updateUserRights verifies update-user-rights); both are available to the authenticated admin attacker — the parent advisory's own PoC shows reading the CSRF token from admin pages. The controller-level absence of an authorization guard is established by source citation (the only gate is userHasPermission(USER_EDIT)), corroborated by the fix diff showing these methods were not modified.

Recommended fix

Apply the overwritePassword invariant to the siblings:

  • editUser(): reject is_superadmin/status/2FA changes unless $this->currentUser->isSuperAdmin(); never allow a non-SuperAdmin to edit a SuperAdmin or protected target. Treat is_superadmin as a SuperAdmin-only field (defeat the mass-assignment at :443/:463).
  • updateUserRights(): require isSuperAdmin() (or a privilege-level comparison) before grantUserRight; forbid granting rights the actor does not itself hold, and forbid targeting SuperAdmin/protected users.
  • activate() (admin/api/user/activate, :194-221) is a lower-impact sibling with the same shape — apply the same guard.

References

@thorsten thorsten published to thorsten/phpMyFAQ May 25, 2026
Published to the GitHub Advisory Database Jun 26, 2026
Reviewed Jun 26, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

EPSS score

Weaknesses

Unlock of a Resource that is not Locked

The product attempts to unlock a resource that is not locked. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-985r-q3qp-299h

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.