GHSA-h4hf-v6w5-897x: Poweradmin API User-update Endpoint Leads To Vulnerability

HIGH
CVSS 8.8
CWE-620 CWE-862
Published
Advisory Summary

GHSA-h4hf-v6w5-897x is a HIGH (CVSS 8.8) api user-update endpoint leads to vulnerability (CWE-620: Unverified Password Change) affecting Poweradmin libraries in the Packagist ecosystem. The issue involves api user-update endpoint leads to a non-admin reset any user's password and take over the superuser account. Remediation is available by upgrading affected packages to version 4.2.5, 4.3.4.

GHSA-h4hf-v6w5-897x at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-h4hf-v6w5-897x
Severity HIGH
CVSS v3.1 Base Score 8.8
CWE Classification CWE-620: Unverified Password Change
Affected Product Poweradmin
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 8.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix GHSA-h4hf-v6w5-897x

To remediate this vulnerability, upgrade all affected Poweradmin dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

poweradmin/poweradmin fixed: 4.2.5
Upgrade poweradmin/poweradmin to 4.2.5
poweradmin/poweradmin fixed: 4.3.4
Upgrade poweradmin/poweradmin to 4.3.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for poweradmin/poweradmin

How to Check if You Are Affected

To determine whether your application is affected by GHSA-h4hf-v6w5-897x, inspect your project dependencies for Poweradmin:

inspect dependencies for poweradmin/poweradmin

Analyze the terminal output to locate any version of Poweradmin in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.2.5), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-620: Unverified Password Change, where when setting a new password for a user, the product does not require knowledge of the original password, or using another form of authentication. In Poweradmin, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary The REST API user-update endpoint (`PUT/PATCH /api/v2/users/{id}` and the V1 equivalent) does not enforce two authorization rules that the web interface enforces. A user who holds the `user_edit_others` permission but is not a superuser can: 1. edit user accounts that belong to a superuser, and 2. set the password of any account, even without the `user_passwd_edit_others` permission. Because of this, a non-admin "user manager" role can send a single API request that changes the administrator's password, then log in as the administrator. This is a full privilege escalation and account takeover. The same actions are explicitly blocked in the web UI, so the API is inconsistent with the application's own permission model. ### Details Poweradmin's permission model treats these as three distinct permissions: - `user_edit_others` (id 57): "User is allowed to edit other users." - `user_passwd_edit_others` (id 58): "User is allowed to edit the password of other users." - `user_is_ueberuser` (id 53): full admin. The existence of a separate `user_passwd_edit_others` permission means that "edit other users" is not supposed to include changing their passwords. The web UI enforces this, and it additionally forbids any non-superuser from editing a superuser account at all. The API skips both rules. **Where the API is missing the superuser check.** `lib/Domain/Service/ApiPermissionService.php`, `canEditUser()`: ```php public function canEditUser(int $userId, int $targetUserId): bool { if ($this->userHasPermission($userId, 'user_is_ueberuser')) { return true; } if ($userId === $targetUserId && $this->userHasPermission($userId, 'user_edit_own')) { return true; } // User with user_edit_others can edit ALL users, including superusers if ($this->userHasPermission($userId, 'user_edit_others')) { return true; } return false; } ``` There is no check on whether the target is a superuser. Compare this with the web controller `lib/Application/Controller/EditUserController.php` (lines 237 to 243), which explicitly refuses: ```php // Prevent non-superusers from editing superuser accounts (privilege escalation protection) $targetIsSuperuser = UserManager::isUserSuperuser($this->db, $editId); $currentIsSuperuser = UserManager::verifyPermission($this->db, 'user_is_ueberuser'); if ($targetIsSuperuser && !$currentIsSuperuser) { $this->showError(_('You do not have permission to edit a superuser account.')); } ``` The comment in the maintainers' own code calls this "privilege escalation protection." The API has no equivalent. **Where the API is missing the password permission check.** `lib/Domain/Service/UserManagementService.php`, `updateUser()` (lines 393 to 413) writes the password whenever one is supplied, with no permission check at all (it only refuses when the target is an external-auth user): ```php if (!empty($userData['password'])) { $user = $this->userRepository->getUserById($userId); $authMethod = $user['auth_method'] ?? 'sql'; $externalAuthMethods = ['oidc', 'saml', 'ldap']; if (in_array($authMethod, $externalAuthMethods, true)) { return [ 'success' => false, 'message' => '...', 'status' => 400 ]; } // Hash password if allowed <-- no permission check happens here $userData['password'] = password_hash($userData['password'], PASSWORD_DEFAULT); } $success = $this->userRepository->updateUser($userId, $userData); ``` Compare the web path `lib/Domain/Model/UserManager.php`, `editUser()`, which only writes the password column when the caller has the right permission: ```php $edit_own_perm = self::verifyPermission($this->db, 'user_edit_own'); $passwd_edit_others_perm = self::verifyPermission($this->db, 'user_passwd_edit_others'); if ($user_password != "" && ($edit_own_perm || $passwd_edit_others_perm)) { ... $query .= ", password = :password"; } ``` **The call path.** `lib/Application/Controller/Api/V2/UsersController.php`, `updateUser()` (lines 701 to 727): ```php if (!$this->apiPermissionService->canEditUser($currentUserId, $targetUserId)) { // line 708 ... 403 ... } $result = $this->userManagementService->updateUser($targetUserId, $input); // line 727 ``` So `canEditUser` returns true for any `user_edit_others` holder against any target, and `updateUser` then writes the password with no further check. The V1 controller (`lib/Application/Controller/Api/V1/UsersController.php`) has the same shape. Note: `perm_templ` is separately protected (a `PermissionTemplateAssignmentGuard` rejects it unless the caller has `user_edit_templ_perm`), so an attacker cannot elevate their own template through the API. They do not need to. Resetting the administrator's password and logging in as the administrator achieves full control directly. <img width="1324" height="986" alt="temp-perm" src="https://github.com/user-attachments/assets/f4411543-2f07-457c-9681-46bdada09d15" /> ### PoC Tested on Poweradmin master (commit 7f28c3a97) and the code path is present in the 4.0.x, 4.1.x, 4.2.x and 4.3.x release branches. **Configuration required (both are common in real deployments, neither is the default):** - The REST API must be enabled (`api.enabled = true` in `config/settings.php`). The API is intended for automation such as the Terraform provider, so it is frequently turned on. - A delegated "user manager" role must exist: a permission template that grants `user_edit_others` (and typically `user_view_others`) but not `user_is_ueberuser` and not `user_passwd_edit_others`. This is a normal way to let a helpdesk or team lead manage user accounts. **Setup (performed once by the administrator to create the delegated role and the attacker):** 1. As admin, create a permission template named "UserMgr" of type "user" with the permissions "User is allowed to edit other users" and "User is allowed to see other users and their details" only. 2. Create a normal user, for example `umgr`, and assign it the UserMgr template. 3. As `umgr`, create an API key from the user's API keys page. Call it, for example, `pwa_umgr_key`. `umgr` is now the attacker: a non-admin user with a self-scoped API key. **Exploit request (Burp Suite Repeater).** Send this single request. `id` 1 is the administrator account. ``` PUT /api/v2/users/1 HTTP/1.1 Host: TARGET_HOST X-API-Key: pwa_umgr_key Content-Type: application/json Content-Length: 30 {"password":"NewAdminPass1!"} ``` Expected response (HTTP 200): ``` {"success":true,"data":{"user_id":1},"message":"User updated successfully"} ``` <img width="1574" height="817" alt="burp-api-request" src="https://github.com/user-attachments/assets/465a365b-511b-4cf5-900d-3779622bb10e" /> now we can login to admin user with a new password! The page refuses with "You do not have permission to edit a superuser account." So the same user with the same permission is denied on the web but allowed on the API. <img width="1228" height="699" alt="umgr-perm-denied" src="https://github.com/user-attachments/assets/5bdb9c82-e00b-4f00-9d25-3887826e5ee3" /> Record ids are the normal sequential user ids, and the administrator is usually id 1, so no guessing is required. The attacker can also change `username`, `email`, `active` and `use_ldap` on any account through the same request. ### Impact This is a broken access control and privilege escalation issue (CWE-285 Improper Authorization, CWE-266 Incorrect Privilege Assignment). Who is impacted: any Poweradmin installation that has the REST API enabled and that has created at least one non-admin role holding `user_edit_others` (a delegated user-manager or helpdesk role). On such an installation, any holder of that role, using nothing more than their own API key, can reset the superuser's password and take over the administrator account. From there they control every zone, record, user and setting in Poweradmin, and, through the PowerDNS backend, the DNS data itself. The severity is high because the outcome is full administrative takeover from a low-privilege authenticated position, over the network, with a single request and no user interaction. The reason it is not rated critical outright is the precondition that a delegated `user_edit_others` role exists and the API is enabled. The underlying defect is real regardless of configuration: the API does not enforce the superuser-edit protection or the `user_passwd_edit_others` permission that the web interface enforces, so the API grants more authority than the permission model intends. Suggested fix: mirror the web guards in the API. In `ApiPermissionService::canEditUser`, reject a non-superuser editing a superuser target. In `UserManagementService::updateUser`, only write the password when the caller is editing their own account or holds `user_passwd_edit_others`. Apply the same to both V1 and V2 controllers. ## Disclosure Credit: Found by Saif Salah (https://saifsalah.github.io/) Found during a security review of Poweradmin. Happy to coordinate a fix timeline and CVE before any public write-up.

GHSA-h4hf-v6w5-897x Affected Poweradmin Versions

2 component(s)
Fixed Versions:
4.2.54.3.4
Product / Package Ecosystem Affected Versions Fixed Version
poweradmin/poweradmin
Packagist
≥ 4.0.0
< 4.2.5
4.2.5
poweradmin/poweradmin
Packagist
≥ 4.3.0
< 4.3.4
4.3.4

References

Frequently Asked Questions

What is GHSA-h4hf-v6w5-897x?
GHSA-h4hf-v6w5-897x is a security vulnerability in Poweradmin involving API user-update endpoint leads to a non-admin reset any user's password and take over the superuser account under CWE-620: Unverified Password Change.
Which Poweradmin versions are affected?
Affected versions include releases prior to 4.2.5, 4.3.4 across affected packages: poweradmin/poweradmin.
What version fixes GHSA-h4hf-v6w5-897x?
Upgrading to 4.2.5, 4.3.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is poweradmin/poweradmin affected?
Yes, poweradmin/poweradmin is affected in releases prior to 4.2.5, 4.3.4. Upgrade your dependency to 4.2.5, 4.3.4 or later.
What is the CVSS score of GHSA-h4hf-v6w5-897x?
GHSA-h4hf-v6w5-897x has a CVSS v3.1 base score of 8.8 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with GHSA-h4hf-v6w5-897x?
GHSA-h4hf-v6w5-897x is classified under CWE-620: Unverified Password Change (When setting a new password for a user, the product does not require knowledge of the original password, or using another form of authentication.).
How can I check Packagist dependencies for GHSA-h4hf-v6w5-897x?
Run the dependency tree command: 'inspect dependencies for poweradmin/poweradmin'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-h4hf-v6w5-897x",
  "modified" : "2026-07-24T21:54:55Z",
  "published" : "2026-07-24T21:54:55Z",
  "aliases" : [ ],
  "summary" : "Poweradmin: API user-update endpoint leads to a non-admin reset any user's password and take over the superuser account",
  "details" : "### Summary\n\nThe REST API user-update endpoint (`PUT/PATCH /api/v2/users/{id}` and the V1 equivalent) does not enforce two authorization rules that the web interface enforces. A user who holds the `user_edit_others` permission but is not a superuser can:\n\n1. edit user accounts that belong to a superuser, and\n2. set the password of any account, even without the `user_passwd_edit_others` permission.\n\nBecause of this, a non-admin \"user manager\" role can send a single API request that changes the administrator's password, then log in as the administrator. This is a full privilege escalation and account takeover. The same actions are explicitly blocked in the web UI, so the API is inconsistent with the application's own permission model.\n\n### Details\n\nPoweradmin's permission model treats these as three distinct permissions:\n\n- `user_edit_others` (id 57): \"User is allowed to edit other users.\"\n- `user_passwd_edit_others` (id 58): \"User is allowed to edit the password of other users.\"\n- `user_is_ueberuser` (id 53): full admin.\n\nThe existence of a separate `user_passwd_edit_others` permission means that \"edit other users\" is not supposed to include changing their passwords. The web UI enforces this, and it additionally forbids any non-superuser from editing a superuser account at all. The API skips both rules.\n\n**Where the API is missing the superuser check.**\n`lib/Domain/Service/ApiPermissionService.php`, `canEditUser()`:\n\n```php\npublic function canEditUser(int $userId, int $targetUserId): bool\n{\n    if ($this->userHasPermission($userId, 'user_is_ueberuser')) {\n        return true;\n    }\n    if ($userId === $targetUserId && $this->userHasPermission($userId, 'user_edit_own')) {\n        return true;\n    }\n    // User with user_edit_others can edit ALL users, including superusers\n    if ($this->userHasPermission($userId, 'user_edit_others')) {\n        return true;\n    }\n    return false;\n}\n```\n\nThere is no check on whether the target is a superuser. Compare this with the web controller `lib/Application/Controller/EditUserController.php` (lines 237 to 243), which explicitly refuses:\n\n```php\n// Prevent non-superusers from editing superuser accounts (privilege escalation protection)\n$targetIsSuperuser = UserManager::isUserSuperuser($this->db, $editId);\n$currentIsSuperuser = UserManager::verifyPermission($this->db, 'user_is_ueberuser');\n\nif ($targetIsSuperuser && !$currentIsSuperuser) {\n    $this->showError(_('You do not have permission to edit a superuser account.'));\n}\n```\n\nThe comment in the maintainers' own code calls this \"privilege escalation protection.\" The API has no equivalent.\n\n**Where the API is missing the password permission check.**\n`lib/Domain/Service/UserManagementService.php`, `updateUser()` (lines 393 to 413) writes the password whenever one is supplied, with no permission check at all (it only refuses when the target is an external-auth user):\n\n```php\nif (!empty($userData['password'])) {\n    $user = $this->userRepository->getUserById($userId);\n    $authMethod = $user['auth_method'] ?? 'sql';\n    $externalAuthMethods = ['oidc', 'saml', 'ldap'];\n    if (in_array($authMethod, $externalAuthMethods, true)) {\n        return [ 'success' => false, 'message' => '...', 'status' => 400 ];\n    }\n    // Hash password if allowed  <-- no permission check happens here\n    $userData['password'] = password_hash($userData['password'], PASSWORD_DEFAULT);\n}\n$success = $this->userRepository->updateUser($userId, $userData);\n```\n\nCompare the web path `lib/Domain/Model/UserManager.php`, `editUser()`, which only writes the password column when the caller has the right permission:\n\n```php\n$edit_own_perm = self::verifyPermission($this->db, 'user_edit_own');\n$passwd_edit_others_perm = self::verifyPermission($this->db, 'user_passwd_edit_others');\nif ($user_password != \"\" && ($edit_own_perm || $passwd_edit_others_perm)) {\n    ...\n    $query .= \", password = :password\";\n}\n```\n\n**The call path.** `lib/Application/Controller/Api/V2/UsersController.php`, `updateUser()` (lines 701 to 727):\n\n```php\nif (!$this->apiPermissionService->canEditUser($currentUserId, $targetUserId)) {   // line 708\n    ... 403 ...\n}\n$result = $this->userManagementService->updateUser($targetUserId, $input);         // line 727\n```\n\nSo `canEditUser` returns true for any `user_edit_others` holder against any target, and `updateUser` then writes the password with no further check. The V1 controller (`lib/Application/Controller/Api/V1/UsersController.php`) has the same shape.\n\nNote: `perm_templ` is separately protected (a `PermissionTemplateAssignmentGuard` rejects it unless the caller has `user_edit_templ_perm`), so an attacker cannot elevate their own template through the API. They do not need to. Resetting the administrator's password and logging in as the administrator achieves full control directly.\n\n<img width=\"1324\" height=\"986\" alt=\"temp-perm\" src=\"https://github.com/user-attachments/assets/f4411543-2f07-457c-9681-46bdada09d15\" />\n\n### PoC\n\nTested on Poweradmin master (commit 7f28c3a97) and the code path is present in the 4.0.x, 4.1.x, 4.2.x and 4.3.x release branches.\n\n**Configuration required (both are common in real deployments, neither is the default):**\n\n- The REST API must be enabled (`api.enabled = true` in `config/settings.php`). The API is intended for automation such as the Terraform provider, so it is frequently turned on.\n- A delegated \"user manager\" role must exist: a permission template that grants `user_edit_others` (and typically `user_view_others`) but not `user_is_ueberuser` and not `user_passwd_edit_others`. This is a normal way to let a helpdesk or team lead manage user accounts.\n\n**Setup (performed once by the administrator to create the delegated role and the attacker):**\n\n1. As admin, create a permission template named \"UserMgr\" of type \"user\" with the permissions \"User is allowed to edit other users\" and \"User is allowed to see other users and their details\" only.\n2. Create a normal user, for example `umgr`, and assign it the UserMgr template.\n3. As `umgr`, create an API key from the user's API keys page. Call it, for example, `pwa_umgr_key`. `umgr` is now the attacker: a non-admin user with a self-scoped API key.\n\n**Exploit request (Burp Suite Repeater).** Send this single request. `id` 1 is the administrator account.\n\n```\nPUT /api/v2/users/1 HTTP/1.1\nHost: TARGET_HOST\nX-API-Key: pwa_umgr_key\nContent-Type: application/json\nContent-Length: 30\n\n{\"password\":\"NewAdminPass1!\"}\n```\n\nExpected response (HTTP 200):\n\n```\n{\"success\":true,\"data\":{\"user_id\":1},\"message\":\"User updated successfully\"}\n```\n\n<img width=\"1574\" height=\"817\" alt=\"burp-api-request\" src=\"https://github.com/user-attachments/assets/465a365b-511b-4cf5-900d-3779622bb10e\" />\n\nnow we can login to admin user with a new password!\n\n\nThe page refuses with \"You do not have permission to edit a superuser account.\" So the same user with the same permission is denied on the web but allowed on the API.\n\n<img width=\"1228\" height=\"699\" alt=\"umgr-perm-denied\" src=\"https://github.com/user-attachments/assets/5bdb9c82-e00b-4f00-9d25-3887826e5ee3\" />\n\n\nRecord ids are the normal sequential user ids, and the administrator is usually id 1, so no guessing is required. The attacker can also change `username`, `email`, `active` and `use_ldap` on any account through the same request.\n\n### Impact\n\nThis is a broken access control and privilege escalation issue (CWE-285 Improper Authorization, CWE-266 Incorrect Privilege Assignment).\n\nWho is impacted: any Poweradmin installation that has the REST API enabled and that has created at least one non-admin role holding `user_edit_others` (a delegated user-manager or helpdesk role). On such an installation, any holder of that role, using nothing more than their own API key, can reset the superuser's password and take over the administrator account. From there they control every zone, record, user and setting in Poweradmin, and, through the PowerDNS backend, the DNS data itself.\n\nThe severity is high because the outcome is full administrative takeover from a low-privilege authenticated position, over the network, with a single request and no user interaction. The reason it is not rated critical outright is the precondition that a delegated `user_edit_others` role exists and the API is enabled. The underlying defect is real regardless of configuration: the API does not enforce the superuser-edit protection or the `user_passwd_edit_others` permission that the web interface enforces, so the API grants more authority than the permission model intends.\n\nSuggested fix: mirror the web guards in the API. In `ApiPermissionService::canEditUser`, reject a non-superuser editing a superuser target. In `UserManagementService::updateUser`, only write the password when the caller is editing their own account or holds `user_passwd_edit_others`. Apply the same to both V1 and V2 controllers.\n\n## Disclosure\nCredit: Found by Saif Salah (https://saifsalah.github.io/)\nFound during a security review of Poweradmin. Happy to coordinate a fix timeline and CVE before any public write-up.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "poweradmin/poweradmin"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.2.5"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "poweradmin/poweradmin"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.3.0"
      }, {
        "fixed" : "4.3.4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/security/advisories/GHSA-h4hf-v6w5-897x"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/commit/36e9081a5c83cb962dc6275b7adcd6cb463f1028"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/commit/9bdda77bc103f283a0babcaf3355f3f95b360122"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/commit/b90fd83e67e6d7b6ad6c9be3a1c0d2f45bd73b6c"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/commit/ffb95d5201cad5cb38e3eabc4f8103cac2ca18e2"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/poweradmin/poweradmin"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/releases/tag/v4.2.5"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/poweradmin/poweradmin/releases/tag/v4.3.4"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-620", "CWE-862" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-24T21:54:55Z",
    "nvd_published_at" : null
  }
}