phpMyFAQ SuperAdmin Privilege Escalation
CVE-2026-57996: A missing authorization guard in phpMyFAQ's user/add API lets delegated admins create SuperAdmin accounts, enabling full instance takeover.
Overview
CVE-2026-57996 is a privilege escalation vulnerability in phpMyFAQ, a widely deployed open-source FAQ management system built on PHP. The flaw resides in the POST /admin/api/user/add REST endpoint, which fails to enforce any server-side guard preventing non-SuperAdmin administrators from creating accounts with SuperAdmin privileges. Any delegated administrator holding the USER_ADD, USER_EDIT, or USER_DELETE permission set can submit a crafted request with isSuperAdmin: true and attacker-controlled credentials, instantly minting a new SuperAdmin account.
The vulnerability was identified by security researchers and disclosed through the phpMyFAQ project’s GitHub Security Advisories program. It was assigned a CVSS v3.1 score of 8.8 (HIGH), reflecting the low attack complexity, the low privilege level required to trigger it (a delegated admin account rather than SuperAdmin), and the complete loss of confidentiality, integrity, and availability that follows a successful exploit. Versions of phpMyFAQ prior to 4.1.5 are affected.
Organizations running phpMyFAQ in multi-tenant or enterprise deployments — where the SuperAdmin role is deliberately scoped to a small set of trusted operators — are at the greatest risk. A rogue delegated administrator, a compromised helpdesk account, or an insider threat can exploit this flaw to silently self-promote to the highest privilege tier without any SuperAdmin interaction.
Technical Analysis
The root cause is a missing authorization check (CWE-862) at the API controller layer. phpMyFAQ’s administration interface exposes a JSON API for user management. When a delegated administrator creates a new user, the request payload includes an isSuperAdmin boolean field. The server-side handler trusts the client-supplied value without verifying that the requesting principal is itself a SuperAdmin.
The vulnerable code pattern follows this structure in the user addition controller:
// src/admin/api/user.php (vulnerable, < 4.1.5)
#[Route('admin/api/user/add', methods: ['POST'])]
public function addUser(Request $request, Response $response): Response
{
$data = json_decode($request->getBody()->getContents());
// Permission check: only requires USER_ADD, not SuperAdmin
if (!$this->currentUser->perm->hasPermission(
$this->currentUser->getUserId(),
'add_user'
)) {
return $this->json(['error' => 'Unauthorized'], 403);
}
$userName = Filter::filterVar($data->userName, FILTER_SANITIZE_SPECIAL_CHARS);
$userEmail = Filter::filterVar($data->userEmail, FILTER_VALIDATE_EMAIL);
$userPassword = $data->userPassword;
// ⚠ VULNERABLE: isSuperAdmin is taken directly from the request payload
// No check that the *requesting* user is themselves a SuperAdmin
$isSuperAdmin = (bool) $data->isSuperAdmin;
$newUser = new User($this->configuration);
$newUser->createUser($userName, $userPassword);
$newUser->userdata->set(['email' => $userEmail]);
$newUser->setSuperAdmin($isSuperAdmin); // ← privilege set from attacker-controlled input
return $this->json(['created' => true, 'userId' => $newUser->getUserId()]);
}
The permission gate on line 6 correctly rejects unauthenticated callers and those lacking add_user, but it never asks: is the requester a SuperAdmin, and are they therefore authorized to grant SuperAdmin status to a new account? The setSuperAdmin() call on the penultimate line blindly promotes the newly created account based entirely on the attacker-supplied boolean.
An attacker exploits this with a single HTTP request:
POST /admin/api/user/add HTTP/1.1
Host: target.example.com
Content-Type: application/json
Cookie: pmf_sid=<delegated_admin_session>
{
"userName": "backdoor",
"userEmail": "[email protected]",
"userPassword": "Str0ng!Pass#2026",
"isSuperAdmin": true
}
The server returns 201 and creates a fully privileged SuperAdmin. The attacker then logs in as backdoor and owns the entire instance.
Impact
A successful exploit produces full application-level compromise:
- Complete administrative takeover: The attacker-created SuperAdmin account can modify all FAQ content, user accounts, system configuration, and SMTP/mail settings.
- Credential harvesting: SuperAdmin access allows modification of login pages or injection of credential-skimming payloads, potentially affecting end-users of the FAQ portal.
- Data exfiltration: All stored FAQ content, registered user PII, and internal configuration data (database credentials in some deployment models) become accessible.
- Persistence: Because the attack creates a legitimate account in the database, the backdoor survives cache flushes, application restarts, and routine session expiry.
- Lateral movement: In environments where phpMyFAQ shares infrastructure with other services (common in intranet deployments), SMTP credentials or database connections exposed through the SuperAdmin panel can serve as pivot points.
The CVSS v3.1 vector AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H accurately reflects that exploitation requires only a network-reachable instance and a low-privilege (delegated admin) account — a bar that is trivially met by any insider or phished helpdesk operator.
How to Fix It
Upgrade immediately to phpMyFAQ 4.1.5 or later. The patch introduces a mandatory SuperAdmin guard before the isSuperAdmin field is honored:
// src/admin/api/user.php (patched, >= 4.1.5)
#[Route('admin/api/user/add', methods: ['POST'])]
public function addUser(Request $request, Response $response): Response
{
$data = json_decode($request->getBody()->getContents());
if (!$this->currentUser->perm->hasPermission(
$this->currentUser->getUserId(),
'add_user'
)) {
return $this->json(['error' => 'Unauthorized'], 403);
}
$userName = Filter::filterVar($data->userName, FILTER_SANITIZE_SPECIAL_CHARS);
$userEmail = Filter::filterVar($data->userEmail, FILTER_VALIDATE_EMAIL);
$userPassword = $data->userPassword;
// ✅ FIXED: only a SuperAdmin can grant SuperAdmin status
$isSuperAdmin = false;
if (isset($data->isSuperAdmin) && (bool) $data->isSuperAdmin) {
if (!$this->currentUser->isSuperAdmin()) {
return $this->json(['error' => 'Insufficient privileges to create SuperAdmin accounts'], 403);
}
$isSuperAdmin = true;
}
$newUser = new User($this->configuration);
$newUser->createUser($userName, $userPassword);
$newUser->userdata->set(['email' => $userEmail]);
$newUser->setSuperAdmin($isSuperAdmin);
return $this->json(['created' => true, 'userId' => $newUser->getUserId()]);
}
The fix follows the principle of least privilege: the privilege being granted cannot exceed the privilege of the grantor.
To upgrade via Composer:
composer require phpmyfaq/phpmyfaq:^4.1.5
php bin/phpmyfaq.php migrate
As a compensating control until upgrade is possible, restrict network access to /admin/api/user/add at the WAF or reverse proxy layer, and audit your current user table for unexpected SuperAdmin accounts:
SELECT user_id, login, is_superadmin, member_since
FROM faquser
WHERE is_superadmin = 1
ORDER BY member_since DESC;
Our Take
This vulnerability is a textbook example of client-supplied privilege fields trusted by the server — a mistake that appears repeatedly across web applications of all technology stacks. The pattern is deceptively easy to introduce: a developer adds a convenience field to a JSON payload during a feature sprint, wires up a basic permission check, and ships. The check validates whether the caller can use the endpoint at all, but never validates whether the caller is authorized for every discrete action the endpoint can perform. The gap between “can create users” and “can create SuperAdmin users” is enormous in impact but invisible in code review without deliberate threat modeling.
For enterprises, the lesson is architectural: privilege elevation paths must be treated as a distinct threat surface, enumerated explicitly in your API contracts, and verified with dedicated authorization assertions — not inferred from the surrounding permission check. Every field that encodes a security-sensitive attribute (isSuperAdmin, role, isAdmin, permissions) is a potential escalation vector if the server doesn’t enforce granular authorization at write time.
Detection with SAST
This vulnerability class maps to CWE-862 (Missing Authorization) and more specifically to the sub-pattern of unauthorized privilege assignment via unguarded API parameters.
Offensive360’s SAST engine detects this class of issue by tracking data flow from untrusted request inputs (JSON body fields, query parameters, form data) through to privilege-assignment sinks — functions and methods that alter a principal’s role, permission set, or administrative status. Concretely, our rules flag:
- Taint paths from
$request->getBody()or$_POSTinto calls such assetSuperAdmin(),setRole(),grantPermission(), or direct ORM writes to role/privilege columns, where no intermediate authorization assertion on the caller’s privilege level exists. - Permission check scope analysis: a permission check that validates only endpoint-level access (
add_user) but not operation-level access (grant_superadmin) is flagged as an incomplete guard. - Boolean field promotion patterns: JSON deserialization of fields whose names match a configurable list of privilege-sensitive identifiers (
superadmin,isAdmin,role,permissions) triggers a medium-confidence finding that is escalated to high when taint reaches a write sink without a caller-privilege assertion in the call graph.
This detection strategy catches the vulnerability regardless of whether the parameter is named isSuperAdmin or obfuscated as level, tier, or accountType — because we track data flow to privilege sinks, not just keyword matching.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-57996-class vulnerabilities and thousands of other patterns — across 60+ languages.