# GHSA-h4hf-v6w5-897x: Poweradmin: API user-update endpoint leads to a non-admin reset any user's password and take over the superuser account

> A low-privileged user can bypass API authorization checks to reset the superuser password and gain full administrative control.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Any signed-in user
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 24 Jul 2026

## What is GHSA-h4hf-v6w5-897x?

GHSA-h4hf-v6w5-897x is a missing authorization vulnerability in poweradmin/poweradmin, published on 24 Jul 2026. Any signed-in user can exploit it over the network, with no action from the victim.

### Official description

### 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 be

## Which versions are affected by GHSA-h4hf-v6w5-897x?

4 open source packages ship an affected version, including poweradmin/poweradmin.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `poweradmin/poweradmin` | Packagist | From 4.0.0 | 4.2.5 |
| `poweradmin/poweradmin` | Packagist | From 4.0.0 | 4.2.5 |
| `poweradmin/poweradmin` | Packagist | From 4.3.0 | 4.3.4 |
| `poweradmin/poweradmin` | Packagist | From 4.3.0 | 4.3.4 |

## How do I fix GHSA-h4hf-v6w5-897x?

Upgrade poweradmin/poweradmin to a fixed version for your release line: 4.2.5 or 4.3.4.

## Is GHSA-h4hf-v6w5-897x being exploited?

There is no known exploitation of GHSA-h4hf-v6w5-897x in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-h4hf-v6w5-897x and fix it if it is affected.

What it is: GHSA-h4hf-v6w5-897x is a missing authorization vulnerability in poweradmin/poweradmin, published on 24 Jul 2026. Any signed-in user can exploit it over the network, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-h4hf-v6w5-897x in the wild and no public exploit code.

Affected packages and fixed versions:
- poweradmin/poweradmin (Packagist): affected from 4.0.0; fixed in 4.2.5
- poweradmin/poweradmin (Packagist): affected from 4.0.0; fixed in 4.2.5
- poweradmin/poweradmin (Packagist): affected from 4.3.0; fixed in 4.3.4
- poweradmin/poweradmin (Packagist): affected from 4.3.0; fixed in 4.3.4

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-h4hf-v6w5-897x/ (Markdown: https://insights.sbomify.com/GHSA-h4hf-v6w5-897x/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-h4hf-v6w5-897x

---
From sbomify Insights: https://insights.sbomify.com/GHSA-h4hf-v6w5-897x/
Summaries are generated and may contain mistakes; the official description above is the record.
