GHSA-985R-Q3QP-299H

Vulnerability from github – Published: 2026-06-26 21:23 – Updated: 2026-06-26 21:23
VLAI
Summary
phpMyFAQ has an incomplete fix for GHSA-xvp4-phqj-cjr3 — editUser() and updateUserRights() lack authorization guards
Details

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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.3"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "thorsten/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.3"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "phpmyfaq/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-832"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-26T21:23:37Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Advisory / Disclosure\n\n# phpMyFAQ 4.1.3 \u2014 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 \u2014 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\u0027 [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` \u2192 `getUserById()` \u2192 privileged mutation\" primitive \u2014 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, \u2026)` | **none** (only `userHasPermission(USER_EDIT)`) |\n\nA logged-in administrator holding the delegable `edit_user` right \u2014 but  **not** SuperAdmin \u2014 can therefore:\n\n1. Set their own (or anyone\u0027s) `is_superadmin` flag to `true` via `admin/api/user/edit` \u2192 **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, \u2026)` at **:511**. Only gate at :485.\n  - `overwritePassword()` lines 419\u2026 \u2192 228-288; the **patched** guards at: 254-260 and: 269-273.\n- `phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php` \u2014`userHasPermission()` :221-227 (checks one right only; non SuperAdmins can hold it).\n- `phpmyfaq/src/phpMyFAQ/User.php` \u2014 `setSuperAdmin()` :950-962 (`UPDATE faquser SET is_superadmin=\u2026 WHERE user_id=\u2026`, no guard); `isSuperAdmin()` :942-945.\n- `phpmyfaq/src/phpMyFAQ/Permission/BasicPermission.php` \u2014 `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\u0027s **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 \u2014`setSuperAdmin` (UPDATE), `grantUserRight` (INSERT), and the real `hasPermission` / `checkUserRight` / `getRightId` queries \u2014 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) \u2014 confirms the fix works *there*.\n- **Exploit 1:** `editor` (non-SuperAdmin, passes `userHasPermission(edit_user)`) flips their own `is_superadmin` 0\u21921 \u2192 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 \u2014 the parent advisory\u0027s 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 \u2014 apply the same guard.",
  "id": "GHSA-985r-q3qp-299h",
  "modified": "2026-06-26T21:23:37Z",
  "published": "2026-06-26T21:23:37Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-985r-q3qp-299h"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/thorsten/phpMyFAQ"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "phpMyFAQ has an incomplete fix for GHSA-xvp4-phqj-cjr3 \u2014 editUser() and updateUserRights() lack authorization guards"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…