GitHub Advisoryadvisory · Packagist
GHSA-cmwh-g2h8-c222 Poweradmin: OIDC `sub` collation bypass in Poweradmin leading to account takeover
HIGH
Official page ↗
- Ecosystem
- Packagist
- Packages
- poweradmin/poweradmin
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-287
- Reviewed
- 2026-07-24
Affected packages
• Packagist poweradmin/poweradmin: >= 4.1.0, < 4.2.5 → fixed in 4.2.5
• Packagist poweradmin/poweradmin: >= 4.3.0, < 4.3.4 → fixed in 4.3.4
Description
Preface
Poweradmin maps OIDC identities into local users through `oidc_user_links.oidc_subject` plus `provider_id`. In the MySQL schema, the OIDC link table explicitly uses `utf8mb4_unicode_ci`, which is case-insensitive and accent-insensitive. OIDC `sub` is a stable external subject identifier and should be matched byte-for-byte within the issuer/provider scope.
The confirmed local PoC used two different OIDC users:
• Victim subject: `victim-login`
• Attacker subject: `victím-login` (`í`, U+00ED)
MySQL reported those two subjects as equal under `utf8mb4_unicode_ci`. After the victim linked their OIDC account, the attacker authenticated to the same provider with the attacker's own password and Poweradmin resolved the session to the victim's local account.
Server Info
• Application: Poweradmin
• Version: `targets/poweradmin` git `e1f9c9a`
• Database: MySQL `8.4.10`, `character_set_server=utf8mb4`, `collation_server=utf8mb4_unicode_ci`
• Access Permissions: Any user who can create or control an account in the connected OIDC provider
• Auth Method: OIDC generic provider
• Tools: Docker Compose, local OIDC provider, Python PoC harness
Affected Entry Point:
```text
GET /oidc/login?provider=generic
GET /oidc/callback?code=...&state=...
```
Relevant request properties:
• Authentication: valid OIDC authorization code flow
• Trigger: attacker OIDC account has a `sub` that collides with a victim's linked `sub`
• Vulnerable field: OIDC `sub` stored and looked up as `oidc_user_links.oidc_subject`
Root Cause Analysis
part0 — `oidc_user_links.oidc_subject` uses an accent-insensitive collation
The MySQL schema defines the OIDC link table with `utf8mb4_unicode_ci`:
```sql
-- sql/poweradmin-mysql-db-structure.sql:341-356
CREATE TABLE `oidc_user_links` (
`user_id` INT(11) NOT NULL,
`provider_id` VARCHAR(50) NOT NULL,
`oidc_subject` VARCHAR(255) NOT NULL,
...
UNIQUE KEY `unique_subject_provider` (`oidc_subject`, `provider_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
In the PoC database:
```text
Field Type Collation
provider_id varchar(50) utf8mb4_unicode_ci
oidc_subject varchar(255) utf8mb4_unicode_ci
SELECT 'victim-login' = 'victím-login' COLLATE utf8mb4_unicode_ci;
-- accent_collision = 1
```
part1 — OIDC `sub` flows into a normal SQL equality lookup
Poweradmin reads the subject from OIDC userinfo data. If no custom `subject` mapping is configured, it uses the `sub` claim:
```php
// lib/Application/Service/OidcService.php:424-459
$resourceOwner = $provider->getResourceOwner($token);
$userData = $resourceOwner->toArray();
...
subject: $userData[$mapping['subject'] ?? 'sub'] ?? '',
```
The callback passes the resulting `OidcUserInfo` to provisioning:
```php
// lib/Application/Service/OidcService.php:269-291
$userInfo = $this->getUserInfo($provider, $token, $providerId);
$userId = $this->userProvisioningService->provisionUser($userInfo, $providerId);
```
Provisioning first tries to find an existing user by subject:
```php
// lib/Application/Service/UserProvisioningService.php:86-90
$existingUserId = $authMethod === self::AUTH_METHOD_SAML
? $this->findUserBySamlSubject($userInfo->getSubject(), $providerId)
: $this->findUserByOidcSubject($userInfo->getSubject(), $providerId);
```
The lookup is normal SQL equality evaluated under the column's weak collation:
```php
// lib/Application/Service/UserProvisioningService.php:145-149
$stmt = $this->db->prepare("
SELECT user_id FROM oidc_user_links
WHERE oidc_subject = ? AND provider_id = ?
");
$stmt->execute([$subject, $providerId]);
```
When the attacker authenticates with `sub = victím-login`, MySQL matches the existing row for `oidc_subject = victim-login` and returns the victim's `user_id`.
part2 — The returned user ID becomes the authenticated session
After provisioning returns the matched `user_id`, Poweradmin fetches the database username for that user and stores the matched user ID in the session:
```php
// lib/Application/Service/OidcService.php:307-317
$databaseUsername = $this->userProvisioningService->getDatabaseUsername($userId);
$this->setSessionValue('userlogin', $databaseUsername);
```
```php
// lib/Application/Service/OidcService.php:360-371
$this->setSessionValue('userid', $userId);
...
$this->setSessionValue('authenticated', true);
```
The attacker's OIDC password is validated by the IdP, but the local `user_id` selected by Poweradmin comes from the weak-collation SQL lookup.
Security Impact
An attacker who can register or control an OIDC principal with an accent/collation variant of a victim's OIDC subject can authenticate with the attacker's own IdP credentials and obtain a Poweradmin session for the victim's local account.
The confirmed PoC used distinct OIDC usernames, subjects, emails, and passwords. The attacker did not know or modify the victim's password.
Reproduction
1. Start the local Poweradmin OIDC lab
```bash
sudo -n docker compose -p poweradminoidcpoc -f poc/work/poweradmin-oidc-collation/docker-compose.yml up -d
```
The lab uses MySQL `utf8mb4_unicode_ci` and a local OIDC provider with two real login accounts:
```text
Victim:
username/sub: victim-login
email: [email protected]
password: VictimPassword123!
Attacker:
username/sub: victím-login
email: [email protected]
password: AttackerPassword123!
```
2. Log in once as the victim through OIDC
Authenticate through `GET /oidc/login?provider=generic` using:
```text
username: victim-login
password: VictimPassword123!
```
Poweradmin creates the victim local user and OIDC link:
```text
users:
id username fullname email
2 victim-login Victim Poweradmin [email protected]
oidc_user_links:
id user_id provider_id oidc_subject
1 2 generic victim-login
```
3. Log in as the attacker OIDC user
Authenticate from a separate browser session with:
```text
username: victím-login
password: AttackerPassword123!
```
Observed result from `poc/work/poweradmin-oidc-collation/run_poc.py`:
```json
{
"attacker_login": {
"selected_idp_user": "attacker",
"selected_idp_username": "victím-login",
"has_session_cookie": true,
"home_contains_victim_username": true,
"home_contains_attacker_username": false
},
"attacker_resolved_to_victim": true
}
```
Database evidence after both logins:
```text
version charset_server collation_server
8.4.10 utf8mb4 utf8mb4_unicode_ci
accent_collision
1
users:
id username fullname email
2 victim-login Victim Poweradmin [email protected]
oidc_user_links:
id user_id provider_id oidc_subject oidc_subject_hex
1 2 generic victim-login 76696374696D2D6C6F67696E
```
The application audit log also records all OIDC login events as the victim user:
```text
user:victim-login operation:login_success auth_method:oidc
```
No local user or OIDC link was created for `victím-login`.
Recommended Fix
Treat OIDC subject identifiers as byte-exact strings.
For MySQL, migrate the OIDC mapping identifiers to a binary or byte-preserving collation:
```sql
ALTER TABLE oidc_user_links
MODIFY COLUMN provider_id varchar(50)
CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
MODIFY COLUMN oidc_subject varchar(255)
CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;
```
Also make lookup queries byte-preserving so patched application code protects existing deployments before schema migrations are complete:
```php
$stmt = $this->db->prepare("
SELECT user_id FROM oidc_user_links
WHERE BINARY oidc_subject = BINARY ?
AND BINARY provider_id = BINARY ?
");
```
Review related identity and authorization lookups:
• `findUserByEmail()` uses `users.email = ?` and can be impacted when `link_by_email` is enabled.
• `findPermissionTemplateByName()` uses `perm_templ.name = ?` for SSO permission template mapping.
• `f…
Related
Weakness · CWE (1)
- CWE-287 Improper Authentication weakness
References
- https://github.com/poweradmin/poweradmin/security/advisories/GHSA-cmwh-g2h8-c222 web
- https://github.com/poweradmin/poweradmin/commit/cbb78f401a9cc77c976939e93b558f39cb738965 web
- https://github.com/poweradmin/poweradmin/commit/f814f9e241a7af742193cb68f80b19867c24ba69 web
- https://github.com/poweradmin/poweradmin package
- https://github.com/poweradmin/poweradmin/releases/tag/v4.2.5 web
- https://github.com/poweradmin/poweradmin/releases/tag/v4.3.4 web