{"id":"GHSA-985r-q3qp-299h","summary":"phpMyFAQ has an incomplete fix for GHSA-xvp4-phqj-cjr3 — editUser() and updateUserRights() lack authorization guards","details":"## Advisory / Disclosure\n\n# phpMyFAQ 4.1.3 — incomplete fix for the admin-API IDOR/privilege-escalation class\n\n**Target:** thorsten/phpMyFAQ (composer: `thorsten/phpmyfaq`, `phpmyfaq/phpmyfaq`)\n**Affected:** \u003c= 4.1.3 (the 4.1.3 security fix is incomplete; siblings remain)\n**Class:** CWE-862 Missing Authorization / CWE-269 Improper Privilege Management / CWE-639 Authorization Bypass Through User-Controlled Key\n**Methodology:** M1 incomplete-fix audit (sibling-walk of the 4.1.3 fix for GHSA-xvp4-phqj-cjr3)\n**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)\n\n## Summary\n\nphpMyFAQ 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:\n\n\u003e \"Only SuperAdmins may change other users' [attributes]. Self-service is\n\u003e always allowed.\" and \"a non-SuperAdmin must never be able to alter a\n\u003e SuperAdmin or protected account.\"\n\nThat 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 `userId` → `getUserById()` → privileged mutation\" primitive — but with a strictly more dangerous sink:\n\n| Endpoint | Route | Sink | Guard in 4.1.3 |\n|----------|-------|------|----------------|\n| `overwritePassword()` | `admin/api/user/overwrite-password` | `changePassword()` | **isSelf + isSuperAdmin + target-protection** (patched) |\n| `editUser()` | `admin/api/user/edit` | `setSuperAdmin((bool)$req.is_superadmin)` | **none** (only `userHasPermission(USER_EDIT)`) |\n| `updateUserRights()` | `admin/api/user/update-rights` | `grantUserRight($req.userId, …)` | **none** (only `userHasPermission(USER_EDIT)`) |\n\nA logged-in administrator holding the delegable `edit_user` right — but  **not** SuperAdmin — can therefore:\n\n1. Set their own (or anyone's) `is_superadmin` flag to `true` via `admin/api/user/edit` → **full privilege escalation to SuperAdmin**.\n2. Grant arbitrary rights to any account via `admin/api/user/update-rights`.\n\nThis is exactly the threat model the parent advisory (GHSA-xvp4) calls out: \"organizations with multiple admin users where not all should have SuperAdmin access.\"\n\n## Anchors (upstream tag 4.1.3)\n\n- `phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/UserController.php`\n  - `editUser()` lines 419-476; user-controlled `userId` at :433, user-controlled `is_superadmin` at :443, sink \n`$user \u003esetSuperAdmin((bool)$isSuperAdmin)` at **:463**. Only gate: `userHasPermission(PermissionType::USER_EDIT)` at :422.\n  - `updateUserRights()` lines 482-520; `userId` at :496, sink `grantUserRight($userId, …)` at **:511**. Only gate at :485.\n  - `overwritePassword()` lines 419… → 228-288; the **patched** guards at: 254-260 and: 269-273.\n- `phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php` —`userHasPermission()` :221-227 (checks one right only; non SuperAdmins can hold it).\n- `phpmyfaq/src/phpMyFAQ/User.php` — `setSuperAdmin()` :950-962 (`UPDATE faquser SET is_superadmin=… WHERE user_id=…`, no guard); `isSuperAdmin()` :942-945.\n- `phpmyfaq/src/phpMyFAQ/Permission/BasicPermission.php` — `hasPermission()` :95-112 (SuperAdmin short-circuits true, else  `checkUserRight`).\n\nEvidence snapshots in this folder:\n`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`.\n\n## Proof of Concept\n\n`poc/poc.php` (run log: `poc/run-log.txt`). Dependency-free: it builds phpMyFAQ's **real schema** (copied verbatim from\n`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:\n\n- Seeds a SuperAdmin (`admin`, id=1) and a non-SuperAdmin admin (`editor`, id=2) granted only `add_user`/`edit_user`/`delete_user`.\n- **Control:** the patched `overwritePassword` guard blocks `editor` changing the SuperAdmin (id=1) — confirms the fix works *there*.\n- **Exploit 1:** `editor` (non-SuperAdmin, passes `userHasPermission(edit_user)`) flips their own `is_superadmin` 0→1 → SuperAdmin. `VULNERABLE`.\n- **Exploit 2:** `editor` grants the `editconfig` right via `updateUserRights`. `VULNERABLE`.\n\nRun:\n```sh\nphp poc/poc.php   # -\u003e RESULT: VULNERABLE ... EXIT 0\n```\n\n### PoC scope (honest)\n\nThe 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.\n\n## Recommended fix\n\nApply the `overwritePassword` invariant to the siblings:\n- `editUser()`: reject `is_superadmin`/status/2FA changes unless `$this-\u003ecurrentUser-\u003eisSuperAdmin()`; 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).\n- `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.\n- `activate()` (`admin/api/user/activate`, :194-221) is a lower-impact sibling with the same shape — apply the same guard.","aliases":["CVE-2026-56396"],"modified":"2026-09-10T03:50:49.089361139Z","published":"2026-06-26T21:23:37Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-06-26T21:23:37Z","nvd_published_at":null,"cwe_ids":["CWE-832"]},"references":[{"type":"WEB","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-985r-q3qp-299h"},{"type":"PACKAGE","url":"https://github.com/thorsten/phpMyFAQ"}],"affected":[{"package":{"name":"thorsten/phpmyfaq","ecosystem":"Packagist","purl":"pkg:composer/thorsten/phpmyfaq"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.1.4"}]}],"versions":["2.10.0-alpha","2.8.0","2.8.0-RC","2.8.0-RC2","2.8.0-RC3","2.8.0-RC4","2.8.0-alpha2","2.8.0-alpha3","2.8.0-beta","2.8.0-beta2","2.8.0-beta3","2.8.1","2.8.10","2.8.11","2.8.12","2.8.13","2.8.14","2.8.15","2.8.16","2.8.17","2.8.18","2.8.19","2.8.2","2.8.20","2.8.21","2.8.22","2.8.23","2.8.24","2.8.25","2.8.26","2.8.27","2.8.28","2.8.29","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.0-alpha","2.9.0-alpha2","2.9.0-alpha3","2.9.0-alpha4","2.9.0-beta","2.9.0-beta2","2.9.0-rc","2.9.0-rc2","2.9.0-rc3","2.9.0-rc4","2.9.1","2.9.10","2.9.11","2.9.12","2.9.13","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6","2.9.7","2.9.8","2.9.9","3.0.0","3.0.0-RC","3.0.0-RC.2","3.0.0-alpha","3.0.0-alpha.2","3.0.0-alpha.3","3.0.0-alpha.4","3.0.0-beta","3.0.0-beta.2","3.0.0-beta.3","3.0.1","3.0.10","3.0.11","3.0.12","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.0-RC","3.1.0-alpha","3.1.0-alpha.2","3.1.0-alpha.3","3.1.0-beta","3.1.1","3.1.10","3.1.11","3.1.12","3.1.13","3.1.14","3.1.15","3.1.16","3.1.17","3.1.18","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.2.0","3.2.0-RC","3.2.0-RC.2","3.2.0-RC.4","3.2.0-alpha","3.2.0-beta","3.2.0-beta.2","3.2.1","3.2.10","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","4.0.0","4.0.0-RC","4.0.0-RC.2","4.0.0-RC.3","4.0.0-RC.4","4.0.0-RC.5","4.0.0-alpha","4.0.0-alpha.2","4.0.0-alpha.3","4.0.0-alpha.4","4.0.0-beta","4.0.0-beta.2","4.0.1","4.0.10","4.0.11","4.0.12","4.0.13","4.0.14","4.0.15","4.0.16","4.0.18","4.0.19","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.0.8","4.0.9","4.1.0","4.1.0-RC","4.1.0-RC.2","4.1.0-RC.4","4.1.0-RC.5","4.1.0-RC.6","4.1.0-RC.7","4.1.0-alpha","4.1.0-alpha.2","4.1.0-alpha.3","4.1.0-beta","4.1.0-beta.2","4.1.1","4.1.2","4.1.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.1.3","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-985r-q3qp-299h/GHSA-985r-q3qp-299h.json"}},{"package":{"name":"phpmyfaq/phpmyfaq","ecosystem":"Packagist","purl":"pkg:composer/phpmyfaq/phpmyfaq"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.1.4"}]}],"versions":["2.10.0-alpha","2.8.0","2.8.0-RC","2.8.0-RC2","2.8.0-RC3","2.8.0-RC4","2.8.0-alpha2","2.8.0-alpha3","2.8.0-beta","2.8.0-beta2","2.8.0-beta3","2.8.1","2.8.10","2.8.11","2.8.12","2.8.13","2.8.14","2.8.15","2.8.16","2.8.17","2.8.18","2.8.19","2.8.2","2.8.20","2.8.21","2.8.22","2.8.23","2.8.24","2.8.25","2.8.26","2.8.27","2.8.28","2.8.29","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.0-alpha","2.9.0-alpha2","2.9.0-alpha3","2.9.0-alpha4","2.9.0-beta","2.9.0-beta2","2.9.0-rc","2.9.0-rc2","2.9.0-rc3","2.9.0-rc4","2.9.1","2.9.10","2.9.11","2.9.12","2.9.13","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6","2.9.7","2.9.8","2.9.9","3.0.0","3.0.0-RC","3.0.0-RC.2","3.0.0-alpha","3.0.0-alpha.2","3.0.0-alpha.3","3.0.0-alpha.4","3.0.0-beta","3.0.0-beta.2","3.0.0-beta.3","3.0.1","3.0.10","3.0.11","3.0.12","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.0-RC","3.1.0-alpha","3.1.0-alpha.2","3.1.0-alpha.3","3.1.0-beta","3.1.1","3.1.10","3.1.11","3.1.12","3.1.13","3.1.14","3.1.15","3.1.16","3.1.17","3.1.18","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.2.0","3.2.0-RC","3.2.0-RC.2","3.2.0-RC.4","3.2.0-alpha","3.2.0-beta","3.2.0-beta.2","3.2.1","3.2.10","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","4.0.0","4.0.0-RC","4.0.0-RC.2","4.0.0-RC.3","4.0.0-RC.4","4.0.0-RC.5","4.0.0-alpha","4.0.0-alpha.2","4.0.0-alpha.3","4.0.0-alpha.4","4.0.0-beta","4.0.0-beta.2","4.0.1","4.0.10","4.0.11","4.0.12","4.0.13","4.0.14","4.0.15","4.0.16","4.0.18","4.0.19","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.0.8","4.0.9","4.1.0","4.1.0-RC","4.1.0-RC.2","4.1.0-RC.4","4.1.0-RC.5","4.1.0-RC.6","4.1.0-RC.7","4.1.0-alpha","4.1.0-alpha.2","4.1.0-alpha.3","4.1.0-beta","4.1.0-beta.2","4.1.1","4.1.2","4.1.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.1.3","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-985r-q3qp-299h/GHSA-985r-q3qp-299h.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"}]}