PT-2026-67596 · Packagist · Poweradmin/Poweradmin

Published

2026-07-24

·

Updated

2026-07-24

CVSS v3.1

8.1

High

VectorAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

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: victim.poweradmin@example.com
 password: VictimPassword123!

Attacker:
 username/sub: victím-login
 email: attacker.poweradmin@example.com
 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  victim.poweradmin@example.com

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  victim.poweradmin@example.com

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.
  • findGroupByName() uses user groups.name = ? for SSO group mapping.
Those fields should either be intentionally documented as case/accent-insensitive or migrated/looked up with byte-preserving semantics where they represent security boundaries.

Patches

Fixed in 4.2.5, 4.3.4, and 4.4.0. OIDC and SAML subject identifiers are now matched byte-for-byte. The fix includes a database migration that changes the collation of the identity link columns, so upgrading requires running the SQL update script for your database in the sql/ directory.

Acknowledge / Credit

whale120 (@whale120 tw), working with DEVCORE Internship Program

Fix

Improper Authentication

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-CMWH-G2H8-C222

Affected Products

Poweradmin/Poweradmin